Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from "react"; | |
| import { useNavigate, Link, useLocation } from "react-router-dom"; | |
| import { z } from "zod"; | |
| import { useForm } from "react-hook-form"; | |
| import { zodResolver } from "@hookform/resolvers/zod"; | |
| import { | |
| Form, | |
| FormControl, | |
| FormField, | |
| FormItem, | |
| FormLabel, | |
| FormMessage, | |
| } from "@/components/ui/form"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Button } from "@/components/ui/button"; | |
| import { | |
| Card, | |
| CardContent, | |
| CardDescription, | |
| CardFooter, | |
| CardHeader, | |
| CardTitle, | |
| } from "@/components/ui/card"; | |
| import Logo from "@/components/ui/logo"; | |
| import { toast } from "@/lib/custom-toast"; | |
| import { authService, userSessionService } from "@/lib/api"; | |
| import { useAuth } from "@/lib/auth-context"; | |
| import { Download, Smartphone } from "lucide-react"; | |
| import CheckInBlockerDialog from "@/components/CheckInBlockerDialog"; | |
| import { attendanceApi } from "@/services/attendanceApi"; | |
| // Form schema and validation | |
| const formSchema = z.object({ | |
| email: z.string().email({ | |
| message: "Please enter a valid email address", | |
| }), | |
| password: z.string().min(1, { | |
| message: "Password is required", | |
| }), | |
| }); | |
| const Login = () => { | |
| const navigate = useNavigate(); | |
| const location = useLocation(); | |
| const from = location.state?.from?.pathname || "/"; | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [apiError, setApiError] = useState<string | null>(null); | |
| const [showPassword, setShowPassword] = useState(false); | |
| const [showCheckInDialog, setShowCheckInDialog] = useState(false); | |
| const [checkInLoading, setCheckInLoading] = useState(false); | |
| const [pendingUserData, setPendingUserData] = useState<any>(null); | |
| const { login } = useAuth(); | |
| // Redirect to dashboard if already logged in | |
| useEffect(() => { | |
| // Removing the auto-redirect so users can log in again if needed | |
| }, []); | |
| // Form setup with validation | |
| const form = useForm<z.infer<typeof formSchema>>({ | |
| resolver: zodResolver(formSchema), | |
| defaultValues: { | |
| email: "", | |
| password: "" | |
| }, | |
| }); | |
| const handleCheckIn = async () => { | |
| if (!pendingUserData) return; | |
| try { | |
| setCheckInLoading(true); | |
| await attendanceApi.checkIn(pendingUserData.employeeId); | |
| toast.success("Checked in successfully!"); | |
| setShowCheckInDialog(false); | |
| // Small delay to ensure backend has processed the check-in | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| login(pendingUserData); | |
| } catch (error: any) { | |
| toast.error(error?.response?.data || error?.message || "Failed to check in"); | |
| console.error('Check-in error:', error); | |
| } finally { | |
| setCheckInLoading(false); | |
| } | |
| }; | |
| const onSubmit = async (values: z.infer<typeof formSchema>) => { | |
| try { | |
| setIsLoading(true); | |
| setApiError(null); | |
| // Call the real API | |
| const response = await authService.login({ | |
| email: values.email, | |
| password: values.password | |
| }); | |
| // Handle successful login | |
| if (response && response.userId) { | |
| // Log token details for debugging | |
| console.log('Login successful - token received:', !!response.token); | |
| if (response.token) { | |
| console.log('Token starts with:', response.token.substring(0, 20) + '...'); | |
| } else { | |
| console.error('WARNING: No token in the login response!'); | |
| } | |
| // Show success message | |
| toast.success(`Welcome back, ${response.firstName}!`); | |
| // Check if user is a client - clients don't need to check in | |
| if (response.roleName?.toLowerCase() === "client") { | |
| login(response); | |
| return; | |
| } | |
| // Check if user has checked in today | |
| try { | |
| const todayAttendance = await attendanceApi.getToday(response.employeeId); | |
| if (!todayAttendance || !todayAttendance.inTime) { | |
| // User hasn't checked in - show blocker dialog | |
| setPendingUserData(response); | |
| setShowCheckInDialog(true); | |
| setIsLoading(false); | |
| } else { | |
| // User already checked in - proceed with login | |
| login(response); | |
| } | |
| } catch (error) { | |
| console.error('Error checking attendance:', error); | |
| // If there's an error checking attendance, proceed with login anyway | |
| login(response); | |
| } | |
| } else { | |
| // If no userId in response | |
| throw new Error('Invalid user credentials'); | |
| } | |
| } catch (error: any) { | |
| setIsLoading(false); | |
| // Set error message | |
| const errorMessage = error.message || "Login failed. Please check your credentials."; | |
| setApiError(errorMessage); | |
| toast.error(errorMessage); | |
| console.error("Login error:", error); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| return ( | |
| <div className="h-full flex flex-col items-center justify-center bg-gradient-to-b from-orange-100 via-white to-white p-4 px-2 sm:px-4"> | |
| <div className="w-full max-w-md"> | |
| <div className="mb-6 sm:mb-10 text-center"> | |
| <div className="flex justify-center mb-4 sm:mb-8"> | |
| <div className="px-4 py-4 sm:px-8 sm:py-6 rounded-2xl bg-white shadow-md hover:shadow-lg transition-all duration-300 animate-fadeIn"> | |
| <Logo | |
| size="md" | |
| className="mb-0.5" | |
| /> | |
| </div> | |
| </div> | |
| <h1 className="text-2xl sm:text-3xl font-bold tracking-tight mb-2 animate-fadeIn animation-delay-200">Welcome back</h1> | |
| <p className="text-sm sm:text-base text-muted-foreground animate-fadeIn animation-delay-300"> | |
| Log in to your account to continue | |
| </p> | |
| </div> | |
| <Card className="shadow-lg border-none overflow-hidden"> | |
| <div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-orange-400 to-coral-600"></div> | |
| <CardHeader className="pb-4"> | |
| <CardTitle className="text-xl">Sign in</CardTitle> | |
| <CardDescription> | |
| Enter your credentials below | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| {apiError && ( | |
| <div className="bg-red-50 border border-red-200 text-red-600 px-4 py-2 rounded-md mb-4 text-sm"> | |
| {apiError} | |
| </div> | |
| )} | |
| <Form {...form}> | |
| <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-3"> | |
| <FormField | |
| control={form.control} | |
| name="email" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel className="text-sm">Email</FormLabel> | |
| <FormControl> | |
| <Input | |
| placeholder="info@synthesys.co.in" | |
| autoComplete="email" | |
| inputMode="email" | |
| {...field} | |
| disabled={isLoading} | |
| className="h-9" | |
| /> | |
| </FormControl> | |
| <FormMessage className="text-xs" /> | |
| </FormItem> | |
| )} | |
| /> | |
| <FormField | |
| control={form.control} | |
| name="password" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel className="text-sm">Password</FormLabel> | |
| <FormControl> | |
| <Input | |
| type="password" | |
| placeholder="Password" | |
| autoComplete="current-password" | |
| {...field} | |
| disabled={isLoading} | |
| className="h-9" | |
| /> | |
| </FormControl> | |
| <FormMessage className="text-xs" /> | |
| </FormItem> | |
| )} | |
| /> | |
| <Button | |
| type="submit" | |
| className="w-full mt-2" | |
| disabled={isLoading} | |
| > | |
| {isLoading ? "Signing in..." : "Sign in"} | |
| </Button> | |
| </form> | |
| </Form> | |
| </CardContent> | |
| <CardFooter className="flex flex-col items-center py-3 gap-2"> | |
| <Button variant="link" className="text-xs text-muted-foreground p-0 h-auto" asChild> | |
| <Link to="/forgot-password">Forgot your password?</Link> | |
| </Button> | |
| </CardFooter> | |
| </Card> | |
| <div className="mt-4 sm:mt-6 text-center text-xs sm:text-sm text-muted-foreground"> | |
| <span>Need an account? </span> | |
| <Button | |
| variant="link" | |
| className="p-0 h-auto text-xs sm:text-sm" | |
| onClick={() => toast.info("Please contact administrator for registration.")} | |
| > | |
| Sign up | |
| </Button> | |
| </div> | |
| {/* Mobile App Download Section */} | |
| <div className="mt-8 pt-6 border-t border-gray-200 animate-fadeIn animation-delay-500"> | |
| <Card className="bg-gradient-to-r from-primary/5 to-primary/10 shadow-sm hover:shadow-md transition-all duration-300"> | |
| <CardContent className="p-4 sm:p-6"> | |
| <div className="flex flex-col sm:flex-row items-center gap-4"> | |
| <div className="bg-primary/10 p-3 rounded-full"> | |
| <Smartphone className="h-8 w-8 text-primary/80" /> | |
| </div> | |
| <div className="flex-1 text-center sm:text-left"> | |
| <h3 className="text-sm sm:text-base font-medium">PM-Tool Mobile App</h3> | |
| <p className="text-xs sm:text-sm text-muted-foreground mt-1"> | |
| Manage your projects on the go with our Android app | |
| </p> | |
| </div> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| className="gap-2 border-primary/20 hover:bg-primary/5" | |
| onClick={() => navigate('/download-app')} | |
| > | |
| <Download className="h-4 w-4" /> | |
| <span>Download</span> | |
| </Button> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </div> | |
| {/* Check-In Blocker Dialog */} | |
| <CheckInBlockerDialog | |
| isOpen={showCheckInDialog} | |
| onCheckIn={handleCheckIn} | |
| isLoading={checkInLoading} | |
| userName={pendingUserData?.firstName || 'User'} | |
| /> | |
| </div> | |
| ); | |
| }; | |
| export default Login; | |