- Add batch_eligibility, batch_claim, and batch_check_and_claim intents to AI classifier so multiple patients can be processed one by one - Add queue processing on insurance-status and claims pages to auto-start the next patient after each check/claim completes - Make patient schema firstName, lastName, phone optional so patients can be created with just member ID + DOB from eligibility checks - Cancel buttons now preserve chat history instead of clearing it - Patient-found card shows Check Eligibility, Eligibility & Appointment Today, and Cancel buttons - Claim service date asks user to pick between latest appointment and today when they differ - Login page subtitle styled with animated gradient Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
241 lines
8.9 KiB
TypeScript
Executable File
241 lines
8.9 KiB
TypeScript
Executable File
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { useEffect, useState } from "react";
|
|
import { useAuth } from "@/hooks/use-auth";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Card } from "@/components/ui/card";
|
|
import { Sparkles } from "lucide-react";
|
|
import { CheckedState } from "@radix-ui/react-checkbox";
|
|
import LoadingScreen from "@/components/ui/LoadingScreen";
|
|
import { useLocation } from "wouter";
|
|
import { LoginFormValues, loginSchema } from "@repo/db/types";
|
|
|
|
const DAILY_PALETTES = [
|
|
{ bg: "#f8fafc", blobs: ["#a78bfa", "#38bdf8", "#f472b6", "#fb923c"], textColor: "#1e293b" },
|
|
{ bg: "#faf5ff", blobs: ["#c084fc", "#22d3ee", "#fb7185", "#facc15"], textColor: "#1e1b4b" },
|
|
{ bg: "#f0f9ff", blobs: ["#60a5fa", "#a78bfa", "#34d399", "#f97316"], textColor: "#0c4a6e" },
|
|
{ bg: "#fdf4ff", blobs: ["#e879f9", "#38bdf8", "#fbbf24", "#4ade80"], textColor: "#4a044e" },
|
|
{ bg: "#f0fdf4", blobs: ["#4ade80", "#818cf8", "#fb923c", "#f472b6"], textColor: "#052e16" },
|
|
{ bg: "#fffbeb", blobs: ["#fbbf24", "#f87171", "#a78bfa", "#22d3ee"], textColor: "#78350f" },
|
|
{ bg: "#f8fafc", blobs: ["#38bdf8", "#e879f9", "#34d399", "#fb923c"], textColor: "#0f172a" },
|
|
{ bg: "#fef2f2", blobs: ["#fb7185", "#a78bfa", "#38bdf8", "#4ade80"], textColor: "#7f1d1d" },
|
|
{ bg: "#ecfdf5", blobs: ["#34d399", "#818cf8", "#f472b6", "#facc15"], textColor: "#064e3b" },
|
|
{ bg: "#f5f3ff", blobs: ["#8b5cf6", "#f472b6", "#22d3ee", "#fb923c"], textColor: "#2e1065" },
|
|
{ bg: "#fff7ed", blobs: ["#fb923c", "#a78bfa", "#22d3ee", "#4ade80"], textColor: "#7c2d12" },
|
|
{ bg: "#f0f9ff", blobs: ["#22d3ee", "#c084fc", "#f97316", "#34d399"], textColor: "#0c4a6e" },
|
|
{ bg: "#fdf2f8", blobs: ["#f472b6", "#60a5fa", "#facc15", "#34d399"], textColor: "#831843" },
|
|
{ bg: "#f8fafc", blobs: ["#818cf8", "#f97316", "#22d3ee", "#f472b6"], textColor: "#1e293b" },
|
|
];
|
|
|
|
function getDailyPalette() {
|
|
const now = new Date();
|
|
const start = new Date(now.getFullYear(), 0, 0);
|
|
const dayOfYear = Math.floor((now.getTime() - start.getTime()) / 86400000);
|
|
return DAILY_PALETTES[dayOfYear % DAILY_PALETTES.length]!;
|
|
}
|
|
|
|
export default function AuthPage() {
|
|
const { isLoading, user, loginMutation } = useAuth();
|
|
const [, navigate] = useLocation();
|
|
const [greeting, setGreeting] = useState("How can I help you today?");
|
|
const palette = getDailyPalette();
|
|
|
|
useEffect(() => {
|
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL_BACKEND ?? "";
|
|
fetch(`${API_BASE_URL}/api/greeting`)
|
|
.then((r) => r.json())
|
|
.then((data) => { if (data.greeting) setGreeting(data.greeting); })
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
const loginForm = useForm<LoginFormValues>({
|
|
resolver: zodResolver(loginSchema),
|
|
defaultValues: {
|
|
username: "",
|
|
password: "",
|
|
rememberMe: false,
|
|
},
|
|
});
|
|
|
|
const onLoginSubmit = (data: LoginFormValues) => {
|
|
loginMutation.mutate({ username: data.username, password: data.password });
|
|
};
|
|
|
|
if (isLoading) {
|
|
return <LoadingScreen />;
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (user) {
|
|
navigate("/insurance-status");
|
|
}
|
|
}, [user, navigate]);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-100 flex items-center justify-center" style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}>
|
|
<div className="w-full max-w-4xl grid grid-cols-1 md:grid-cols-2 shadow-xl rounded-2xl overflow-hidden">
|
|
{/* Auth Form */}
|
|
<Card className="p-8 bg-white">
|
|
<div className="mb-10 text-center">
|
|
<h1 className="text-3xl font-bold text-primary mb-3 tracking-tight">
|
|
My Dental Office Management
|
|
</h1>
|
|
<p className="text-sm font-semibold tracking-[0.15em] bg-clip-text text-transparent animate-gradient-flash"
|
|
style={{ backgroundImage: "linear-gradient(90deg, #7c3aed, #d946ef, #06b6d4, #7c3aed)", backgroundSize: "200% 100%" }}>
|
|
Driven By Multiple AI Agents
|
|
</p>
|
|
</div>
|
|
|
|
<Form {...loginForm}>
|
|
<form
|
|
onSubmit={loginForm.handleSubmit(onLoginSubmit)}
|
|
className="space-y-4"
|
|
>
|
|
<FormField
|
|
control={loginForm.control}
|
|
name="username"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Username</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="Enter your username" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={loginForm.control}
|
|
name="password"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Password</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="••••••••"
|
|
type="password"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<FormField
|
|
control={loginForm.control}
|
|
name="rememberMe"
|
|
render={({ field }) => (
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="remember-me"
|
|
checked={field.value as CheckedState}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
<label
|
|
htmlFor="remember-me"
|
|
className="text-sm font-medium text-gray-700"
|
|
>
|
|
Remember me
|
|
</label>
|
|
</div>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
type="submit"
|
|
className="w-full"
|
|
disabled={loginMutation.isPending}
|
|
>
|
|
{loginMutation.isPending ? "Signing in..." : "Sign in"}
|
|
</Button>
|
|
</form>
|
|
</Form>
|
|
</Card>
|
|
|
|
{/* Hero Section — Stripe-style vibrant gradient mesh */}
|
|
<div
|
|
className="relative overflow-hidden md:flex p-8 flex flex-col justify-center items-center min-h-[320px]"
|
|
style={{ background: palette.bg }}
|
|
>
|
|
{/* Animated blobs */}
|
|
<div className="absolute inset-0 login-blob-container">
|
|
<div
|
|
className="absolute opacity-70 login-blob"
|
|
style={{
|
|
width: 300, height: 300,
|
|
top: "-15%", left: "-10%",
|
|
background: palette.blobs[0],
|
|
filter: "blur(70px)",
|
|
}}
|
|
/>
|
|
<div
|
|
className="absolute opacity-70 login-blob animation-delay-2000"
|
|
style={{
|
|
width: 350, height: 350,
|
|
top: "50%", right: "-15%",
|
|
background: palette.blobs[1],
|
|
filter: "blur(70px)",
|
|
}}
|
|
/>
|
|
<div
|
|
className="absolute opacity-70 login-blob animation-delay-4000"
|
|
style={{
|
|
width: 280, height: 280,
|
|
bottom: "-15%", left: "15%",
|
|
background: palette.blobs[2],
|
|
filter: "blur(70px)",
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* 4th accent blob */}
|
|
<div
|
|
className="absolute opacity-50 login-blob animation-delay-2000"
|
|
style={{
|
|
width: 220, height: 220,
|
|
top: "15%", right: "20%",
|
|
background: palette.blobs[3],
|
|
filter: "blur(60px)",
|
|
}}
|
|
/>
|
|
|
|
{/* Content */}
|
|
<div className="relative z-10 flex flex-col items-center">
|
|
<div className="flex justify-center mb-8">
|
|
<div
|
|
className="w-20 h-20 backdrop-blur-md rounded-full flex items-center justify-center border"
|
|
style={{
|
|
background: "rgba(255,255,255,0.25)",
|
|
borderColor: "rgba(255,255,255,0.4)",
|
|
}}
|
|
>
|
|
<Sparkles className="h-10 w-10" style={{ color: palette.textColor }} />
|
|
</div>
|
|
</div>
|
|
<p
|
|
className="text-center text-2xl leading-relaxed font-bold tracking-tight"
|
|
style={{ color: palette.textColor }}
|
|
>
|
|
{greeting}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|