mirror of
https://github.com/FranP-code/inbox-negotiator.git
synced 2025-10-13 00:42:26 +00:00
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:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -25,4 +25,7 @@ pnpm-debug.log*
|
|||||||
|
|
||||||
# supabase files
|
# supabase files
|
||||||
supabase/.branches
|
supabase/.branches
|
||||||
supabase/.temp
|
supabase/.temp
|
||||||
|
|
||||||
|
# vercel files
|
||||||
|
.vercel
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { supabase, type Debt, type UserProfile } from "../lib/supabase";
|
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 { DebtCard } from "./DebtCard";
|
||||||
import { DebtTimeline } from "./DebtTimeline";
|
import { DebtTimeline } from "./DebtTimeline";
|
||||||
import { OnboardingDialog } from "./OnboardingDialog";
|
import { OnboardingDialog } from "./OnboardingDialog";
|
||||||
@@ -10,10 +10,10 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "./ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "./ui/separator";
|
||||||
import {
|
import {
|
||||||
DollarSign,
|
DollarSign,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
@@ -315,7 +315,7 @@ export function Dashboard() {
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
{debtList.map((debt) => (
|
{debtList.map((debt) => (
|
||||||
<div key={debt.id} className="space-y-4">
|
<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">
|
<Card className="bg-gray-50 dark:bg-gray-800/50">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<DebtTimeline debt={debt} />
|
<DebtTimeline debt={debt} />
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "./ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
import { Input } from "./ui/input";
|
||||||
|
import { Textarea } from "./ui/textarea";
|
||||||
|
import { Label } from "./ui/label";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -15,12 +18,21 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "./ui/dialog";
|
||||||
import { Calendar, DollarSign, Mail, FileText, TrendingUp } from "lucide-react";
|
import {
|
||||||
|
Calendar,
|
||||||
|
DollarSign,
|
||||||
|
Mail,
|
||||||
|
FileText,
|
||||||
|
TrendingUp,
|
||||||
|
Edit3,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { supabase } from "../lib/supabase";
|
||||||
import type { Debt } from "../lib/supabase";
|
import type { Debt } from "../lib/supabase";
|
||||||
|
|
||||||
interface DebtCardProps {
|
interface DebtCardProps {
|
||||||
debt: Debt;
|
debt: Debt;
|
||||||
|
onUpdate?: () => void; // Callback to refresh data after updates
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColors = {
|
const statusColors = {
|
||||||
@@ -44,7 +56,7 @@ const statusLabels = {
|
|||||||
opted_out: "Opted Out",
|
opted_out: "Opted Out",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DebtCard({ debt }: DebtCardProps) {
|
export function DebtCard({ debt, onUpdate }: DebtCardProps) {
|
||||||
const formatCurrency = (amount: number) => {
|
const formatCurrency = (amount: number) => {
|
||||||
return new Intl.NumberFormat("en-US", {
|
return new Intl.NumberFormat("en-US", {
|
||||||
style: "currency",
|
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 (
|
return (
|
||||||
<Card className="w-full hover:shadow-lg transition-all duration-300 border-l-4 border-l-primary">
|
<Card className="w-full hover:shadow-lg transition-all duration-300 border-l-4 border-l-primary">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
@@ -124,29 +415,7 @@ export function DebtCard({ debt }: DebtCardProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{debt.negotiated_plan && (
|
{debt.metadata?.aiEmail && <EditableResponseDialog />}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user