From 1239e0f6f38ce8dfa207171f6725d3467476f4e0 Mon Sep 17 00:00:00 2001 From: Francisco Pessano Date: Sat, 7 Jun 2025 14:43:58 -0300 Subject: [PATCH] Refactor DebtCard and Dashboard components for improved UI structure and functionality; add editable response dialog for AI-generated negotiation emails. --- .gitignore | 5 +- src/components/Dashboard.tsx | 12 +- src/components/DebtCard.tsx | 329 +++++++++++++++++++++++++++++++---- 3 files changed, 309 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index a40b315..fd8dbba 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,7 @@ pnpm-debug.log* # supabase files supabase/.branches -supabase/.temp \ No newline at end of file +supabase/.temp + +# vercel files +.vercel \ No newline at end of file diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index b366dc9..1eaae3f 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import { supabase, type Debt, type UserProfile } from "../lib/supabase"; -import { Button } from "@/components/ui/button"; +import { Button } from "./ui/button"; import { DebtCard } from "./DebtCard"; import { DebtTimeline } from "./DebtTimeline"; import { OnboardingDialog } from "./OnboardingDialog"; @@ -10,10 +10,10 @@ import { CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Badge } from "@/components/ui/badge"; -import { Separator } from "@/components/ui/separator"; +} from "./ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; +import { Badge } from "./ui/badge"; +import { Separator } from "./ui/separator"; import { DollarSign, TrendingUp, @@ -315,7 +315,7 @@ export function Dashboard() {
{debtList.map((debt) => (
- + diff --git a/src/components/DebtCard.tsx b/src/components/DebtCard.tsx index 7b03a8f..ad7218a 100644 --- a/src/components/DebtCard.tsx +++ b/src/components/DebtCard.tsx @@ -1,13 +1,16 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +} from "./ui/card"; +import { Badge } from "./ui/badge"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Textarea } from "./ui/textarea"; +import { Label } from "./ui/label"; import { Dialog, DialogContent, @@ -15,12 +18,21 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "@/components/ui/dialog"; -import { Calendar, DollarSign, Mail, FileText, TrendingUp } from "lucide-react"; +} from "./ui/dialog"; +import { + Calendar, + DollarSign, + Mail, + FileText, + TrendingUp, + Edit3, +} from "lucide-react"; +import { supabase } from "../lib/supabase"; import type { Debt } from "../lib/supabase"; interface DebtCardProps { debt: Debt; + onUpdate?: () => void; // Callback to refresh data after updates } const statusColors = { @@ -44,7 +56,7 @@ const statusLabels = { opted_out: "Opted Out", }; -export function DebtCard({ debt }: DebtCardProps) { +export function DebtCard({ debt, onUpdate }: DebtCardProps) { const formatCurrency = (amount: number) => { return new Intl.NumberFormat("en-US", { style: "currency", @@ -62,6 +74,285 @@ export function DebtCard({ debt }: DebtCardProps) { }); }; + // Extract variables from text in {{ variable }} format + const extractVariables = (text: string): string[] => { + const variableRegex = /\{\{\s*([^}]+)\s*\}\}/g; + const matches: string[] = []; + let match; + while ((match = variableRegex.exec(text)) !== null) { + if (!matches.includes(match[1].trim())) { + matches.push(match[1].trim()); + } + } + return matches; + }; + + // Replace variables in text + const replaceVariables = ( + text: string, + variables: Record + ): string => { + let result = text; + Object.entries(variables).forEach(([key, value]) => { + const regex = new RegExp( + `\\{\\{\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\}\\}`, + "g" + ); + result = result.replace(regex, value); + }); + return result; + }; + + const EditableResponseDialog = () => { + const [isEditing, setIsEditing] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [subject, setSubject] = useState(""); + const [body, setBody] = useState(""); + const [variables, setVariables] = useState>({}); + + // Initialize data when dialog opens + useEffect(() => { + if (debt.metadata?.aiEmail) { + const aiEmail = debt.metadata.aiEmail; + setSubject(aiEmail.subject || ""); + setBody(aiEmail.body || ""); + + // Extract variables from both subject and body + const allText = `${aiEmail.subject || ""} ${aiEmail.body || ""}`; + const extractedVars = extractVariables(allText); + const initialVariables: Record = {}; + extractedVars.forEach((variable) => { + initialVariables[variable] = ""; + }); + setVariables(initialVariables); + } + }, [debt.metadata?.aiEmail]); + + // Update variables when body changes + const handleBodyChange = (newBody: string) => { + setBody(newBody); + // Extract variables from the new body text + const newVariables = extractVariables(newBody); + const updatedVariables = { ...variables }; + + // Add new variables if they don't exist + newVariables.forEach((variable) => { + if (!(variable in updatedVariables)) { + updatedVariables[variable] = ""; + } + }); + + // Remove variables that no longer exist in the text + Object.keys(updatedVariables).forEach((variable) => { + if ( + !newVariables.includes(variable) && + !extractVariables(subject).includes(variable) + ) { + delete updatedVariables[variable]; + } + }); + + setVariables(updatedVariables); + }; + + // Update variables when subject changes + const handleSubjectChange = (newSubject: string) => { + setSubject(newSubject); + // Extract variables from the new subject text + const newVariables = extractVariables(newSubject); + const updatedVariables = { ...variables }; + + // Add new variables if they don't exist + newVariables.forEach((variable) => { + if (!(variable in updatedVariables)) { + updatedVariables[variable] = ""; + } + }); + + // Remove variables that no longer exist in the text + Object.keys(updatedVariables).forEach((variable) => { + if ( + !newVariables.includes(variable) && + !extractVariables(body).includes(variable) + ) { + delete updatedVariables[variable]; + } + }); + + setVariables(updatedVariables); + }; + + // Update variables only (don't modify the text) + const handleVariableChange = (variableName: string, value: string) => { + const newVariables = { ...variables, [variableName]: value }; + setVariables(newVariables); + }; + + // Get preview text with variables replaced + const getPreviewText = () => { + return replaceVariables(`Subject: ${subject}\n\n${body}`, variables); + }; + + // Get display text for subject (for preview in input) + const getSubjectDisplay = () => { + return replaceVariables(subject, variables); + }; + + // Get display text for body (for preview in textarea) + const getBodyDisplay = () => { + return replaceVariables(body, variables); + }; + + // Save changes to database + const handleSave = async () => { + setIsSaving(true); + try { + // Update the metadata with the new subject and body + const updatedMetadata = { + ...debt.metadata, + aiEmail: { + ...debt.metadata?.aiEmail, + subject, + body, + }, + }; + + const { error } = await supabase + .from("debts") + .update({ metadata: updatedMetadata }) + .eq("id", debt.id); + + if (error) { + console.error("Error saving changes:", error); + // You could add toast notification here + return; + } + + // Call onUpdate callback to refresh the parent component + if (onUpdate) { + onUpdate(); + } + + setIsEditing(false); + } catch (error) { + console.error("Error saving changes:", error); + } finally { + setIsSaving(false); + } + }; + + if (!debt.metadata?.aiEmail) return null; + + return ( + + + + + + + Edit Negotiation Response + + Customize your FDCPA-compliant response before sending + + + +
+ {/* Variables Form */} + {Object.keys(variables).length > 0 && ( +
+
+ +

+ Complete the placeholders below to personalize your message +

+
+ +
+ {Object.entries(variables).map(([variableName, value]) => ( +
+ + + handleVariableChange(variableName, e.target.value) + } + placeholder={`Enter ${variableName.toLowerCase()}`} + className="w-full" + /> +
+ ))} +
+
+ )} + + {/* Divider */} +
+ + {/* Subject Line */} +
+ + handleSubjectChange(e.target.value)} + placeholder="Enter email subject (use {{ Variable Name }} for placeholders)" + className="w-full" + /> + {/* Preview of subject with variables replaced */} + {Object.keys(variables).length > 0 && ( +
+ Preview: {getSubjectDisplay()} +
+ )} +
+ + {/* Body */} +
+ +