mirror of
https://github.com/FranP-code/inbox-negotiator.git
synced 2025-10-13 00:42:26 +00:00
Adds email approval & Postmark integration for negotiations
Enables users to approve and send negotiation emails directly via Postmark after configuring a server token in settings. Introduces new debt statuses ("approved", "sent"), UI for token management, and approval/rejection flows. Refactors notifications to use a modern toast library, adjusts dashboard status filters, and updates DB schema for new flows.
Empowers compliant, user-controlled negotiation and automated email delivery.
This commit is contained in:
@@ -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<UserProfile | null>(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() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Email Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Mail className="h-5 w-5" />
|
||||
Email Settings
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure your Postmark server token to enable sending approved
|
||||
negotiation emails
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server_token">Postmark Server Token</Label>
|
||||
<Input
|
||||
id="server_token"
|
||||
type="password"
|
||||
placeholder="Enter your Postmark server token"
|
||||
value={serverToken}
|
||||
onChange={(e) => setServerToken(e.target.value)}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Required to send approved negotiation emails. You can get this
|
||||
from your{" "}
|
||||
<a
|
||||
href="https://account.postmarkapp.com/servers"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Postmark dashboard
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={saveServerToken}
|
||||
disabled={savingServerToken}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
{savingServerToken ? "Saving..." : "Save Token"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Personal Data */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
|
||||
@@ -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<any>(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<Record<string, string>>({});
|
||||
|
||||
// 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) {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<Edit3 className="h-4 w-4 mr-2" />
|
||||
Edit Response
|
||||
{isReadOnly ? (
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
) : (
|
||||
<Edit3 className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{isReadOnly ? "See Response" : "Edit Response"}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Negotiation Response</DialogTitle>
|
||||
<DialogTitle>
|
||||
{isReadOnly
|
||||
? "View Negotiation Response"
|
||||
: "Edit Negotiation Response"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Customize your FDCPA-compliant response before sending
|
||||
{isReadOnly
|
||||
? "Review your FDCPA-compliant response"
|
||||
: "Customize your FDCPA-compliant response before sending"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -383,9 +424,12 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) {
|
||||
<Input
|
||||
id="subject"
|
||||
value={subject}
|
||||
onChange={(e) => 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) {
|
||||
<Textarea
|
||||
id="body"
|
||||
value={body}
|
||||
onChange={(e) => handleBodyChange(e.target.value)}
|
||||
onChange={(e) =>
|
||||
!isReadOnly && handleBodyChange(e.target.value)
|
||||
}
|
||||
placeholder="Enter email body (use {{ Variable Name }} for placeholders)"
|
||||
className="w-full min-h-[300px] font-mono text-sm"
|
||||
readOnly={isReadOnly}
|
||||
/>
|
||||
{/* Preview of body with variables replaced */}
|
||||
{Object.keys(variables).length > 0 && (
|
||||
@@ -417,20 +464,183 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) {
|
||||
</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>
|
||||
{!isReadOnly && (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
// Check if user has server token configured
|
||||
useEffect(() => {
|
||||
checkServerToken();
|
||||
}, []);
|
||||
|
||||
const checkServerToken = async () => {
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("postmark_server_token")
|
||||
.eq("user_id", user.id)
|
||||
.single();
|
||||
|
||||
setUserProfile(profile);
|
||||
setHasServerToken(!!profile?.postmark_server_token);
|
||||
} catch (error) {
|
||||
console.error("Error checking server token:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle approve action
|
||||
const handleApprove = async (sendEmail = true) => {
|
||||
if (!hasServerToken && sendEmail) {
|
||||
toast.error("Server Token Required", {
|
||||
description:
|
||||
"Please configure your Postmark server token in settings first.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsApproving(true);
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user) throw new Error("User not authenticated");
|
||||
|
||||
if (sendEmail) {
|
||||
// Call the send-email function
|
||||
const { data, error } = await supabase.functions.invoke("send-email", {
|
||||
body: {
|
||||
debtId: debt.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
if (data.requiresConfiguration) {
|
||||
toast.error("Configuration Required", {
|
||||
description:
|
||||
"Please set up your Postmark server token in configuration.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Email Sent Successfully", {
|
||||
description: `Negotiation email sent to ${data.sentTo}`,
|
||||
});
|
||||
} else {
|
||||
// Call the approve-debt function to handle approval without sending email
|
||||
const { data, error } = await supabase.functions.invoke(
|
||||
"approve-debt",
|
||||
{
|
||||
body: {
|
||||
debtId: debt.id,
|
||||
approvalNote: "Approved by user without sending email",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast.success("Debt Approved", {
|
||||
description: `Negotiation for ${data.vendor} has been approved and saved.`,
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh the data
|
||||
if (onUpdate) onUpdate();
|
||||
} catch (error: any) {
|
||||
console.error("Error in approval process:", error);
|
||||
const action = sendEmail ? "send email" : "approve debt";
|
||||
toast.error(`Failed to ${action}`, {
|
||||
description:
|
||||
error.message || `An error occurred while trying to ${action}.`,
|
||||
});
|
||||
} finally {
|
||||
setIsApproving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle reject action
|
||||
const handleReject = async () => {
|
||||
setIsRejecting(true);
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from("debts")
|
||||
.update({
|
||||
status: "opted_out",
|
||||
metadata: {
|
||||
...debt.metadata,
|
||||
rejected: {
|
||||
rejectedAt: new Date().toISOString(),
|
||||
reason: "User rejected negotiation",
|
||||
},
|
||||
},
|
||||
})
|
||||
.eq("id", debt.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Log the action
|
||||
await supabase.from("audit_logs").insert({
|
||||
debt_id: debt.id,
|
||||
action: "negotiation_rejected",
|
||||
details: {
|
||||
rejectedAt: new Date().toISOString(),
|
||||
reason: "User rejected negotiation",
|
||||
},
|
||||
});
|
||||
|
||||
toast.success("Negotiation Rejected", {
|
||||
description: "The negotiation has been marked as rejected.",
|
||||
});
|
||||
|
||||
// Refresh the data
|
||||
if (onUpdate) onUpdate();
|
||||
} catch (error: any) {
|
||||
console.error("Error rejecting negotiation:", error);
|
||||
toast.error("Failed to Reject", {
|
||||
description:
|
||||
error.message || "An error occurred while rejecting the negotiation.",
|
||||
});
|
||||
} finally {
|
||||
setIsRejecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if approve button should be enabled
|
||||
const canApprove = (sendEmail = true) => {
|
||||
if (sendEmail) {
|
||||
return (
|
||||
debt.metadata?.aiEmail &&
|
||||
debt.status === "negotiating" &&
|
||||
hasServerToken
|
||||
);
|
||||
} else {
|
||||
return debt.metadata?.aiEmail && debt.status === "negotiating";
|
||||
}
|
||||
};
|
||||
|
||||
// Check if buttons should be shown
|
||||
const showApproveRejectButtons = () => {
|
||||
return debt.metadata?.aiEmail && debt.status === "negotiating";
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full hover:shadow-lg transition-all duration-300 border-l-4 border-l-primary">
|
||||
<CardHeader className="pb-3">
|
||||
@@ -470,31 +680,141 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<Mail className="h-4 w-4 mr-2" />
|
||||
View Email
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Original Email</DialogTitle>
|
||||
<DialogDescription>
|
||||
From: {debt.vendor} • {formatDate(debt.created_at)}
|
||||
</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.raw_email || "No email content available"}
|
||||
</pre>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<Mail className="h-4 w-4 mr-2" />
|
||||
View Email
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Original Email</DialogTitle>
|
||||
<DialogDescription>
|
||||
From: {debt.vendor} • {formatDate(debt.created_at)}
|
||||
</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.raw_email || "No email content available"}
|
||||
</pre>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{debt.metadata?.aiEmail && <EditableResponseDialog />}
|
||||
{debt.metadata?.aiEmail && <EditableResponseDialog />}
|
||||
</div>
|
||||
|
||||
{/* Approve/Reject Buttons */}
|
||||
{showApproveRejectButtons() && (
|
||||
<div className="space-y-2">
|
||||
{!hasServerToken && (
|
||||
<div className="flex items-center gap-2 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
|
||||
<AlertCircle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||
<span className="text-sm text-amber-700 dark:text-amber-300 flex-1">
|
||||
Configure your Postmark server token to enable email
|
||||
sending.
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => (window.location.href = "/configuration")}
|
||||
className="text-amber-700 border-amber-300 hover:bg-amber-100"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3 mr-1" />
|
||||
Settings
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 bg-green-600 hover:bg-green-700 text-white"
|
||||
disabled={!canApprove(true) || isApproving}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
{isApproving ? "Sending..." : "Approve & Send"}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Send Negotiation Email
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will send the approved negotiation email to{" "}
|
||||
{debt.vendor}. Make sure you have reviewed and are
|
||||
satisfied with the email content.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => handleApprove()}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
Send Email
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => handleApprove(false)}
|
||||
disabled={isApproving || isRejecting || !canApprove(false)}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Approve
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reject Button - only show if debt is not approved or sent */}
|
||||
{!isReadOnly && (
|
||||
<div className="flex justify-end gap-2">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
disabled={isRejecting}
|
||||
>
|
||||
<XCircle className="h-4 w-4 mr-2" />
|
||||
{isRejecting ? "Rejecting..." : "Reject"}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Reject Negotiation</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will mark the negotiation as rejected and opt you out
|
||||
of this debt collection process. This action cannot be
|
||||
undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleReject}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
Reject
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React, { useState } from "react";
|
||||
import { supabase } 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 {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
} from "./ui/dialog";
|
||||
import { Alert, AlertDescription } from "./ui/alert";
|
||||
import { Mail, Plus, CheckCircle, ArrowRight, UserCheck } from "lucide-react";
|
||||
import { toast } from "../hooks/use-toast";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface OnboardingDialogProps {
|
||||
open: boolean;
|
||||
@@ -63,10 +63,8 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
|
||||
setStep("email");
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "Error",
|
||||
toast.error("Error", {
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -102,10 +100,8 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
|
||||
setStep("complete");
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "Error",
|
||||
toast.error("Error", {
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
Reference in New Issue
Block a user