From 1ecc722b6308d377bfedfaa4939e47e8d41ad455 Mon Sep 17 00:00:00 2001 From: Francisco Pessano Date: Sat, 7 Jun 2025 13:10:52 -0300 Subject: [PATCH] Add personal data management to user configuration and onboarding --- src/components/Configuration.tsx | 204 +++++++++++++++++ src/components/OnboardingDialog.tsx | 210 +++++++++++++++++- src/components/ui/input.tsx | 36 +-- .../20250607007000_add_personal_data.sql | 38 ++++ 4 files changed, 464 insertions(+), 24 deletions(-) create mode 100644 supabase/migrations/20250607007000_add_personal_data.sql diff --git a/src/components/Configuration.tsx b/src/components/Configuration.tsx index 7413cf9..1f752d7 100644 --- a/src/components/Configuration.tsx +++ b/src/components/Configuration.tsx @@ -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() { + {/* Personal Data */} + + + + + Personal Information + + + Your personal information used in negotiation letters + + + +
+
+ + + setPersonalData({ + ...personalData, + full_name: e.target.value, + }) + } + /> +
+ +
+ + + setPersonalData({ + ...personalData, + address_line_1: e.target.value, + }) + } + /> +
+ +
+ + + setPersonalData({ + ...personalData, + address_line_2: e.target.value, + }) + } + /> +
+ +
+
+ + + setPersonalData({ + ...personalData, + city: e.target.value, + }) + } + /> +
+
+ + + setPersonalData({ + ...personalData, + state: e.target.value, + }) + } + /> +
+
+ +
+
+ + + setPersonalData({ + ...personalData, + zip_code: e.target.value, + }) + } + /> +
+
+ + + setPersonalData({ + ...personalData, + phone_number: e.target.value, + }) + } + /> +
+
+
+ +
+ +
+
+
+ {/* Additional Emails */} diff --git a/src/components/OnboardingDialog.tsx b/src/components/OnboardingDialog.tsx index 15c1e3f..4c29298 100644 --- a/src/components/OnboardingDialog.tsx +++ b/src/components/OnboardingDialog.tsx @@ -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) { */}
- @@ -119,6 +168,153 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) { )} + {step === "personal" && ( + <> + + + + Personal Information + + + + Please provide your personal information. This will be used to + generate formal negotiation letters. + + + All fields are optional, but providing complete information will + result in more professional letters. + + +
+
+
+ + + setPersonalData({ + ...personalData, + full_name: e.target.value, + }) + } + /> +
+ +
+ + + setPersonalData({ + ...personalData, + address_line_1: e.target.value, + }) + } + /> +
+ +
+ + + setPersonalData({ + ...personalData, + address_line_2: e.target.value, + }) + } + /> +
+ +
+
+ + + setPersonalData({ + ...personalData, + city: e.target.value, + }) + } + /> +
+
+ + + setPersonalData({ + ...personalData, + state: e.target.value, + }) + } + /> +
+
+ +
+
+ + + setPersonalData({ + ...personalData, + zip_code: e.target.value, + }) + } + /> +
+
+ + + setPersonalData({ + ...personalData, + phone_number: e.target.value, + }) + } + /> +
+
+
+ +
+ + +
+
+ + )} + {step === "email" && ( <> @@ -176,11 +372,11 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) { Skip for now
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index c982112..e3c90b9 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -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 {} + extends React.InputHTMLAttributes {} const Input = React.forwardRef( - ({ className, type, ...props }, ref) => { - return ( - - ); - } + ({ className, type, ...props }, ref) => { + return ( +
+ +
+ ); + } ); -Input.displayName = 'Input'; +Input.displayName = "Input"; export { Input }; diff --git a/supabase/migrations/20250607007000_add_personal_data.sql b/supabase/migrations/20250607007000_add_personal_data.sql new file mode 100644 index 0000000..5fbab34 --- /dev/null +++ b/supabase/migrations/20250607007000_add_personal_data.sql @@ -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();