Refactor DebtCard and Dashboard components for improved UI structure and functionality; add editable response dialog for AI-generated negotiation emails.

This commit is contained in:
2025-06-07 14:43:58 -03:00
parent fc6964c996
commit 1239e0f6f3
3 changed files with 309 additions and 37 deletions

5
.gitignore vendored
View File

@@ -25,4 +25,7 @@ pnpm-debug.log*
# supabase files
supabase/.branches
supabase/.temp
supabase/.temp
# vercel files
.vercel

View File

@@ -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() {
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{debtList.map((debt) => (
<div key={debt.id} className="space-y-4">
<DebtCard debt={debt} />
<DebtCard debt={debt} onUpdate={fetchDebts} />
<Card className="bg-gray-50 dark:bg-gray-800/50">
<CardContent className="p-4">
<DebtTimeline debt={debt} />

View File

@@ -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, string>
): 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<Record<string, string>>({});
// 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<string, string> = {};
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 (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="sm" className="flex-1">
<Edit3 className="h-4 w-4 mr-2" />
Edit Response
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit Negotiation Response</DialogTitle>
<DialogDescription>
Customize your FDCPA-compliant response before sending
</DialogDescription>
</DialogHeader>
<div className="space-y-6 mt-4">
{/* Variables Form */}
{Object.keys(variables).length > 0 && (
<div className="space-y-4">
<div className="border-t pt-4">
<Label className="text-base font-semibold">
Fill in Variables
</Label>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Complete the placeholders below to personalize your message
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
{Object.entries(variables).map(([variableName, value]) => (
<div key={variableName} className="space-y-2">
<Label
htmlFor={`var-${variableName}`}
className="text-sm"
>
{variableName}
</Label>
<Input
id={`var-${variableName}`}
value={value}
onChange={(e) =>
handleVariableChange(variableName, e.target.value)
}
placeholder={`Enter ${variableName.toLowerCase()}`}
className="w-full"
/>
</div>
))}
</div>
</div>
)}
{/* Divider */}
<div className="border-t pt-4" />
{/* Subject Line */}
<div className="space-y-2">
<Label htmlFor="subject">Subject Line</Label>
<Input
id="subject"
value={subject}
onChange={(e) => 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 && (
<div className="text-sm text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 p-2 rounded border">
<strong>Preview:</strong> {getSubjectDisplay()}
</div>
)}
</div>
{/* Body */}
<div className="space-y-2">
<Label htmlFor="body">Email Body</Label>
<Textarea
id="body"
value={body}
onChange={(e) => handleBodyChange(e.target.value)}
placeholder="Enter email body (use {{ Variable Name }} for placeholders)"
className="w-full min-h-[300px] font-mono text-sm"
/>
{/* Preview of body with variables replaced */}
{Object.keys(variables).length > 0 && (
<div className="text-sm text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 p-3 rounded border">
<strong>Preview:</strong>
<pre className="whitespace-pre-wrap mt-2 font-mono text-xs">
{getBodyDisplay()}
</pre>
</div>
)}
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-2 border-t pt-4">
<Button variant="outline" onClick={() => setIsEditing(false)}>
Cancel
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? "Saving..." : "Save Changes"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};
return (
<Card className="w-full hover:shadow-lg transition-all duration-300 border-l-4 border-l-primary">
<CardHeader className="pb-3">
@@ -124,29 +415,7 @@ export function DebtCard({ debt }: DebtCardProps) {
</DialogContent>
</Dialog>
{debt.negotiated_plan && (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="sm" className="flex-1">
<FileText className="h-4 w-4 mr-2" />
View Response
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>AI-Generated Negotiation Response</DialogTitle>
<DialogDescription>
FDCPA-compliant response ready to send
</DialogDescription>
</DialogHeader>
<div className="mt-4">
<pre className="whitespace-pre-wrap text-sm bg-gray-50 dark:bg-gray-800 p-4 rounded-lg border dark:border-gray-700">
{debt.negotiated_plan}
</pre>
</div>
</DialogContent>
</Dialog>
)}
{debt.metadata?.aiEmail && <EditableResponseDialog />}
</div>
</CardContent>
</Card>