diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..0b9f410 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,13 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Dev Server", + "type": "shell", + "command": "pnpm dev", + "group": "build", + "isBackground": true, + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/src/components/Configuration.tsx b/src/components/Configuration.tsx index 1f752d7..f3f50f3 100644 --- a/src/components/Configuration.tsx +++ b/src/components/Configuration.tsx @@ -5,20 +5,20 @@ import { type UserProfile, type EmailProcessingUsage, } from "../lib/supabase"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Progress } from "@/components/ui/progress"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Separator } from "@/components/ui/separator"; +} from "./ui/card"; +import { Badge } from "./ui/badge"; +import { Progress } from "./ui/progress"; +import { Alert, AlertDescription } from "./ui/alert"; +import { Separator } from "./ui/separator"; import { Settings, Mail, @@ -31,7 +31,7 @@ import { AlertCircle, UserCheck, } from "lucide-react"; -import { toast } from "../hooks/use-toast"; +import { toast } from "sonner"; export function Configuration() { const [profile, setProfile] = useState(null); @@ -55,6 +55,10 @@ export function Configuration() { }); const [savingPersonalData, setSavingPersonalData] = useState(false); + // Server token state + const [serverToken, setServerToken] = useState(""); + const [savingServerToken, setSavingServerToken] = useState(false); + useEffect(() => { fetchUserData(); }, []); @@ -112,6 +116,11 @@ export function Configuration() { phone_number: userData.phone_number || "", }); } + + // Set server token + if (profileData) { + setServerToken(profileData.postmark_server_token || ""); + } } catch (error) { console.error("Error fetching user data:", error); } finally { @@ -142,21 +151,52 @@ export function Configuration() { if (error) throw error; - toast({ - title: "Personal data updated", + toast.success("Personal data updated", { description: "Your personal information has been saved successfully.", }); } catch (error: any) { - toast({ - title: "Error saving personal data", + toast.error("Error saving personal data", { description: error.message, - variant: "destructive", }); } finally { setSavingPersonalData(false); } }; + const saveServerToken = async () => { + setSavingServerToken(true); + try { + const { + data: { user }, + } = await supabase.auth.getUser(); + if (!user) return; + + const { error } = await supabase + .from("user_profiles") + .update({ + postmark_server_token: serverToken || null, + }) + .eq("user_id", user.id); + + if (error) throw error; + + // Update local profile state + setProfile((prev) => + prev ? { ...prev, postmark_server_token: serverToken || null } : null + ); + + toast.success("Server token updated", { + description: "Your Postmark server token has been saved successfully.", + }); + } catch (error: any) { + toast.error("Error saving server token", { + description: error.message, + }); + } finally { + setSavingServerToken(false); + } + }; + const addAdditionalEmail = async () => { if (!newEmail || !profile) return; @@ -180,15 +220,12 @@ export function Configuration() { setAdditionalEmails([data, ...additionalEmails]); setNewEmail(""); - toast({ - title: "Email added successfully", + toast.success("Email added successfully", { description: "Additional email has been added to your account.", }); } catch (error: any) { - toast({ - title: "Error adding email", + toast.error("Error adding email", { description: error.message, - variant: "destructive", }); } finally { setAddingEmail(false); @@ -207,15 +244,12 @@ export function Configuration() { setAdditionalEmails( additionalEmails.filter((email) => email.id !== emailId) ); - toast({ - title: "Email removed", + toast.success("Email removed", { description: "Additional email has been removed from your account.", }); } catch (error: any) { - toast({ - title: "Error removing email", + toast.error("Error removing email", { description: error.message, - variant: "destructive", }); } }; @@ -318,6 +352,55 @@ export function Configuration() { + {/* Email Settings */} + + + + + Email Settings + + + Configure your Postmark server token to enable sending approved + negotiation emails + + + +
+ + setServerToken(e.target.value)} + /> +

+ Required to send approved negotiation emails. You can get this + from your{" "} + + Postmark dashboard + + . +

+
+ +
+ +
+
+
+ {/* Personal Data */} diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 1eaae3f..1c753b8 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -169,7 +169,9 @@ export function Dashboard() { active: debts.filter((debt) => ["received", "negotiating"].includes(debt.status) ), - settled: debts.filter((debt) => debt.status === "settled"), + settled: debts.filter((debt) => + ["settled", "approved", "sent"].includes(debt.status) + ), failed: debts.filter((debt) => ["failed", "opted_out"].includes(debt.status) ), diff --git a/src/components/DebtCard.tsx b/src/components/DebtCard.tsx index 62d3df1..2b97c43 100644 --- a/src/components/DebtCard.tsx +++ b/src/components/DebtCard.tsx @@ -19,6 +19,17 @@ import { DialogTitle, DialogTrigger, } from "./ui/dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "./ui/alert-dialog"; import { Calendar, DollarSign, @@ -26,9 +37,14 @@ import { FileText, TrendingUp, Edit3, + CheckCircle, + XCircle, + AlertCircle, + ExternalLink, + Eye, } from "lucide-react"; import { supabase, type Debt, type DebtVariable } from "../lib/supabase"; -import { toast } from "../hooks/use-toast"; +import { toast } from "sonner"; interface DebtCardProps { debt: Debt; @@ -40,6 +56,9 @@ const statusColors = { "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-800", negotiating: "bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-300 dark:border-yellow-800", + approved: + "bg-teal-100 text-teal-800 border-teal-200 dark:bg-teal-900/20 dark:text-teal-300 dark:border-teal-800", + sent: "bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-900/20 dark:text-purple-300 dark:border-purple-800", settled: "bg-green-100 text-green-800 border-green-200 dark:bg-green-900/20 dark:text-green-300 dark:border-green-800", failed: @@ -51,12 +70,25 @@ const statusColors = { const statusLabels = { received: "Received", negotiating: "Negotiating", + approved: "Approved", + sent: "Sent", settled: "Settled", failed: "Failed", opted_out: "Opted Out", }; export function DebtCard({ debt, onUpdate }: DebtCardProps) { + const [isApproving, setIsApproving] = useState(false); + const [isRejecting, setIsRejecting] = useState(false); + const [userProfile, setUserProfile] = useState(null); + const [hasServerToken, setHasServerToken] = useState(false); + + const isReadOnly = + debt.status === "approved" || + debt.status === "sent" || + debt.status === "failed" || + debt.status === "opted_out"; + const formatCurrency = (amount: number) => { return new Intl.NumberFormat("en-US", { style: "currency", @@ -110,6 +142,8 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) { const [body, setBody] = useState(""); const [variables, setVariables] = useState>({}); + // Check if debt is in read-only state (approved or sent) + // Load variables from database const loadVariables = async () => { try { @@ -284,10 +318,8 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) { if (error) { console.error("Error saving debt metadata:", error); - toast({ - title: "Error", + toast.error("Error", { description: "Failed to save email changes. Please try again.", - variant: "destructive", }); return; } @@ -295,8 +327,7 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) { // Save variables to database await saveVariables(variables); - toast({ - title: "Changes saved", + toast.success("Changes saved", { description: "Your email and variables have been updated successfully.", }); @@ -309,10 +340,8 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) { setIsEditing(false); } catch (error) { console.error("Error saving changes:", error); - toast({ - title: "Error", + toast.error("Error", { description: "Failed to save changes. Please try again.", - variant: "destructive", }); } finally { setIsSaving(false); @@ -325,15 +354,25 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) { - Edit Negotiation Response + + {isReadOnly + ? "View Negotiation Response" + : "Edit Negotiation Response"} + - Customize your FDCPA-compliant response before sending + {isReadOnly + ? "Review your FDCPA-compliant response" + : "Customize your FDCPA-compliant response before sending"} @@ -363,10 +402,12 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) { id={`var-${variableName}`} value={value} onChange={(e) => + !isReadOnly && handleVariableChange(variableName, e.target.value) } placeholder={`Enter ${variableName.toLowerCase()}`} className="w-full" + readOnly={isReadOnly} /> ))} @@ -383,9 +424,12 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) { handleSubjectChange(e.target.value)} + onChange={(e) => + !isReadOnly && handleSubjectChange(e.target.value) + } placeholder="Enter email subject (use {{ Variable Name }} for placeholders)" className="w-full" + readOnly={isReadOnly} /> {/* Preview of subject with variables replaced */} {Object.keys(variables).length > 0 && ( @@ -401,9 +445,12 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) {