mirror of
https://github.com/FranP-code/inbox-negotiator.git
synced 2025-10-13 00:42:26 +00:00
Add personal data management to user configuration and onboarding
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
|
||||
38
supabase/migrations/20250607007000_add_personal_data.sql
Normal file
38
supabase/migrations/20250607007000_add_personal_data.sql
Normal 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();
|
||||
Reference in New Issue
Block a user