Add personal data management to user configuration and onboarding

This commit is contained in:
2025-06-07 13:10:52 -03:00
parent aa287e424d
commit 1ecc722b63
4 changed files with 464 additions and 24 deletions

View File

@@ -29,6 +29,7 @@ import {
TrendingUp,
Infinity,
AlertCircle,
UserCheck,
} from "lucide-react";
import { toast } from "../hooks/use-toast";
@@ -42,6 +43,18 @@ export function Configuration() {
const [newEmail, setNewEmail] = useState("");
const [addingEmail, setAddingEmail] = useState(false);
// Personal data state
const [personalData, setPersonalData] = useState({
full_name: "",
address_line_1: "",
address_line_2: "",
city: "",
state: "",
zip_code: "",
phone_number: "",
});
const [savingPersonalData, setSavingPersonalData] = useState(false);
useEffect(() => {
fetchUserData();
}, []);
@@ -60,6 +73,13 @@ export function Configuration() {
.eq("user_id", user.id)
.single();
// Fetch user personal data
const { data: userData } = await supabase
.from("users")
.select("*")
.eq("id", user.id)
.single();
// Fetch additional emails
const { data: emailsData } = await supabase
.from("additional_emails")
@@ -79,6 +99,19 @@ export function Configuration() {
setProfile(profileData);
setAdditionalEmails(emailsData || []);
setUsage(usageData);
// Set personal data
if (userData) {
setPersonalData({
full_name: userData.full_name || "",
address_line_1: userData.address_line_1 || "",
address_line_2: userData.address_line_2 || "",
city: userData.city || "",
state: userData.state || "",
zip_code: userData.zip_code || "",
phone_number: userData.phone_number || "",
});
}
} catch (error) {
console.error("Error fetching user data:", error);
} finally {
@@ -86,6 +119,44 @@ export function Configuration() {
}
};
const savePersonalData = async () => {
setSavingPersonalData(true);
try {
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) return;
const { error } = await supabase
.from("users")
.update({
full_name: personalData.full_name || null,
address_line_1: personalData.address_line_1 || null,
address_line_2: personalData.address_line_2 || null,
city: personalData.city || null,
state: personalData.state || null,
zip_code: personalData.zip_code || null,
phone_number: personalData.phone_number || null,
})
.eq("id", user.id);
if (error) throw error;
toast({
title: "Personal data updated",
description: "Your personal information has been saved successfully.",
});
} catch (error: any) {
toast({
title: "Error saving personal data",
description: error.message,
variant: "destructive",
});
} finally {
setSavingPersonalData(false);
}
};
const addAdditionalEmail = async () => {
if (!newEmail || !profile) return;
@@ -247,6 +318,139 @@ export function Configuration() {
</CardContent>
</Card>
{/* Personal Data */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<UserCheck className="h-5 w-5" />
Personal Information
</CardTitle>
<CardDescription>
Your personal information used in negotiation letters
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label htmlFor="config_full_name">Full Name</Label>
<Input
id="config_full_name"
placeholder="John Doe"
value={personalData.full_name}
onChange={(e) =>
setPersonalData({
...personalData,
full_name: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="config_address_line_1">Address Line 1</Label>
<Input
id="config_address_line_1"
placeholder="123 Main Street"
value={personalData.address_line_1}
onChange={(e) =>
setPersonalData({
...personalData,
address_line_1: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="config_address_line_2">Address Line 2</Label>
<Input
id="config_address_line_2"
placeholder="Apt 4B"
value={personalData.address_line_2}
onChange={(e) =>
setPersonalData({
...personalData,
address_line_2: e.target.value,
})
}
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-2">
<Label htmlFor="config_city">City</Label>
<Input
id="config_city"
placeholder="New York"
value={personalData.city}
onChange={(e) =>
setPersonalData({
...personalData,
city: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="config_state">State</Label>
<Input
id="config_state"
placeholder="NY"
value={personalData.state}
onChange={(e) =>
setPersonalData({
...personalData,
state: e.target.value,
})
}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-2">
<Label htmlFor="config_zip_code">Zip Code</Label>
<Input
id="config_zip_code"
placeholder="10001"
value={personalData.zip_code}
onChange={(e) =>
setPersonalData({
...personalData,
zip_code: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="config_phone_number">Phone Number</Label>
<Input
id="config_phone_number"
placeholder="(555) 123-4567"
value={personalData.phone_number}
onChange={(e) =>
setPersonalData({
...personalData,
phone_number: e.target.value,
})
}
/>
</div>
</div>
</div>
<div className="flex justify-end">
<Button
onClick={savePersonalData}
disabled={savingPersonalData}
className="min-w-[120px]"
>
{savingPersonalData ? "Saving..." : "Save Changes"}
</Button>
</div>
</CardContent>
</Card>
{/* Additional Emails */}
<Card>
<CardHeader>

View File

@@ -20,13 +20,61 @@ interface OnboardingDialogProps {
}
export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
const [step, setStep] = useState<"welcome" | "email" | "complete">("welcome");
const [step, setStep] = useState<
"welcome" | "personal" | "email" | "complete"
>("welcome");
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const [skipEmail, setSkipEmail] = useState(false);
const handleAddEmail = async () => {
if (!email && !skipEmail) return;
// Personal data state
const [personalData, setPersonalData] = useState({
full_name: "",
address_line_1: "",
address_line_2: "",
city: "",
state: "",
zip_code: "",
phone_number: "",
});
const handleSavePersonalData = async () => {
setLoading(true);
try {
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) return;
const { error } = await supabase
.from("users")
.update({
full_name: personalData.full_name || null,
address_line_1: personalData.address_line_1 || null,
address_line_2: personalData.address_line_2 || null,
city: personalData.city || null,
state: personalData.state || null,
zip_code: personalData.zip_code || null,
phone_number: personalData.phone_number || null,
})
.eq("id", user.id);
if (error) throw error;
setStep("email");
} catch (error: any) {
toast({
title: "Error",
description: error.message,
variant: "destructive",
});
} finally {
setLoading(false);
}
};
const handleAddEmail = async (_skipEmail?: boolean) => {
if (!email && !_skipEmail) return;
setLoading(true);
try {
@@ -35,7 +83,7 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
} = await supabase.auth.getUser();
if (!user) return;
if (email && !skipEmail) {
if (email && !_skipEmail) {
const { error } = await supabase.from("additional_emails").insert({
user_id: user.id,
email_address: email.trim().toLowerCase(),
@@ -70,6 +118,7 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
const handleSkipEmail = () => {
setSkipEmail(true);
handleAddEmail(true);
};
return (
@@ -110,7 +159,7 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
</div> */}
<div className="flex justify-end">
<Button onClick={() => setStep("email")}>
<Button onClick={() => setStep("personal")}>
Get Started
<ArrowRight className="h-4 w-4 ml-2" />
</Button>
@@ -119,6 +168,153 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
</>
)}
{step === "personal" && (
<>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<UserCheck className="h-6 w-6 text-primary" />
Personal Information
</DialogTitle>
</DialogHeader>
<DialogDescription>
Please provide your personal information. This will be used to
generate formal negotiation letters.
</DialogDescription>
<DialogDescription className="text-sm text-muted-foreground">
All fields are optional, but providing complete information will
result in more professional letters.
</DialogDescription>
<div className="space-y-4 overflow-y-auto">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label htmlFor="full_name">Full Name</Label>
<Input
id="full_name"
placeholder="John Doe"
value={personalData.full_name}
onChange={(e) =>
setPersonalData({
...personalData,
full_name: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="address_line_1">Address Line 1</Label>
<Input
id="address_line_1"
placeholder="123 Main Street"
value={personalData.address_line_1}
onChange={(e) =>
setPersonalData({
...personalData,
address_line_1: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="address_line_2">Address Line 2</Label>
<Input
id="address_line_2"
placeholder="Apt 4B"
value={personalData.address_line_2}
onChange={(e) =>
setPersonalData({
...personalData,
address_line_2: e.target.value,
})
}
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-2">
<Label htmlFor="city">City</Label>
<Input
id="city"
placeholder="New York"
value={personalData.city}
onChange={(e) =>
setPersonalData({
...personalData,
city: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="state">State</Label>
<Input
id="state"
placeholder="NY"
value={personalData.state}
onChange={(e) =>
setPersonalData({
...personalData,
state: e.target.value,
})
}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-2">
<Label htmlFor="zip_code">Zip Code</Label>
<Input
id="zip_code"
placeholder="10001"
value={personalData.zip_code}
onChange={(e) =>
setPersonalData({
...personalData,
zip_code: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="phone_number">Phone Number</Label>
<Input
id="phone_number"
placeholder="(555) 123-4567"
value={personalData.phone_number}
onChange={(e) =>
setPersonalData({
...personalData,
phone_number: e.target.value,
})
}
/>
</div>
</div>
</div>
<div className="flex gap-2 justify-end">
<Button
variant="outline"
onClick={() => setStep("email")}
disabled={loading}
>
Skip for now
</Button>
<Button
onClick={handleSavePersonalData}
disabled={loading}
className="min-w-[100px]"
>
{loading ? "Saving..." : "Continue"}
</Button>
</div>
</div>
</>
)}
{step === "email" && (
<>
<DialogHeader>
@@ -176,11 +372,11 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
Skip for now
</Button>
<Button
onClick={handleAddEmail}
onClick={() => handleAddEmail()}
disabled={!email || loading}
className="min-w-[100px]"
>
{loading ? "Adding..." : "Add Email"}
{loading && email ? "Adding..." : "Add Email"}
</Button>
</div>
</div>

View File

@@ -1,25 +1,27 @@
import * as React from 'react';
import * as React from "react";
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
);
}
({ className, type, ...props }, ref) => {
return (
<div className="flex justify-center">
<input
type={type}
className={cn(
"flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 w-[calc(100%-8px)]",
className
)}
ref={ref}
{...props}
/>
</div>
);
}
);
Input.displayName = 'Input';
Input.displayName = "Input";
export { Input };

View File

@@ -0,0 +1,38 @@
-- Add personal data to the users table for debt negotiation letters
-- This will store the user's personal information needed for generating formal negotiation letters
-- Add personal data columns to the public.users table
ALTER TABLE public.users ADD COLUMN IF NOT EXISTS full_name TEXT;
ALTER TABLE public.users ADD COLUMN IF NOT EXISTS address_line_1 TEXT;
ALTER TABLE public.users ADD COLUMN IF NOT EXISTS address_line_2 TEXT;
ALTER TABLE public.users ADD COLUMN IF NOT EXISTS city TEXT;
ALTER TABLE public.users ADD COLUMN IF NOT EXISTS state TEXT;
ALTER TABLE public.users ADD COLUMN IF NOT EXISTS zip_code TEXT;
ALTER TABLE public.users ADD COLUMN IF NOT EXISTS phone_number TEXT;
ALTER TABLE public.users ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();
-- Create index for performance
CREATE INDEX IF NOT EXISTS idx_users_updated_at ON public.users(updated_at);
-- Update RLS policy to allow users to update their own data
CREATE POLICY "Users can update own profile" ON public.users
FOR UPDATE USING (auth.uid() = id);
-- Grant UPDATE permission to authenticated users
GRANT UPDATE ON public.users TO authenticated;
-- Create function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_users_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Create trigger for updated_at
DROP TRIGGER IF EXISTS update_users_updated_at ON public.users;
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON public.users
FOR EACH ROW
EXECUTE FUNCTION update_users_updated_at_column();