Merge branch 'Add-gorgias-api'
This commit is contained in:
@@ -23,6 +23,7 @@ import ProductGrid from "./components/dashboard/ProductGrid";
|
||||
import SalesChart from "./components/dashboard/SalesChart";
|
||||
import KlaviyoCampaigns from "@/components/dashboard/KlaviyoCampaigns";
|
||||
import MetaCampaigns from "@/components/dashboard/MetaCampaigns";
|
||||
import GorgiasOverview from "@/components/dashboard/GorgiasOverview";
|
||||
|
||||
// Public layout
|
||||
const PublicLayout = () => (
|
||||
@@ -90,7 +91,6 @@ const DashboardLayout = () => {
|
||||
</div>
|
||||
<Navigation />
|
||||
<div className="p-4 space-y-4">
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-6 gap-4">
|
||||
<div className="xl:col-span-4 col-span-6">
|
||||
<div className="space-y-4 h-full w-full">
|
||||
@@ -122,6 +122,9 @@ const DashboardLayout = () => {
|
||||
<div id="meta-campaigns">
|
||||
<MetaCampaigns />
|
||||
</div>
|
||||
<div id="gorgias-overview">
|
||||
<GorgiasOverview />
|
||||
</div>
|
||||
<div id="calls">
|
||||
<AircallDashboard />
|
||||
</div>
|
||||
|
||||
@@ -351,7 +351,7 @@ const AircallDashboard = () => {
|
||||
<CardHeader className="p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">Aircall Analytics</CardTitle>
|
||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">Calls</CardTitle>
|
||||
{lastUpdated && (
|
||||
<CardDescription className="mt-1">
|
||||
Last updated: {new Date(lastUpdated).toLocaleString()}
|
||||
|
||||
551
dashboard/src/components/dashboard/GorgiasOverview.jsx
Normal file
551
dashboard/src/components/dashboard/GorgiasOverview.jsx
Normal file
@@ -0,0 +1,551 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Clock,
|
||||
Star,
|
||||
MessageSquare,
|
||||
Mail,
|
||||
Send,
|
||||
Loader2,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
Zap,
|
||||
Timer,
|
||||
BarChart3,
|
||||
Bot,
|
||||
ClipboardCheck,
|
||||
} from "lucide-react";
|
||||
import axios from "axios";
|
||||
|
||||
const TIME_RANGES = {
|
||||
"today": "Today",
|
||||
"7": "Last 7 Days",
|
||||
"14": "Last 14 Days",
|
||||
"30": "Last 30 Days",
|
||||
"90": "Last 90 Days",
|
||||
};
|
||||
|
||||
const formatDuration = (seconds) => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
||||
};
|
||||
|
||||
const getDateRange = (days) => {
|
||||
// Create date in Eastern Time
|
||||
const now = new Date();
|
||||
const easternTime = new Date(
|
||||
now.toLocaleString("en-US", { timeZone: "America/New_York" })
|
||||
);
|
||||
|
||||
if (days === "today") {
|
||||
// For today, set the range to be the current day in Eastern Time
|
||||
const start = new Date(easternTime);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
|
||||
const end = new Date(easternTime);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
|
||||
return {
|
||||
start_datetime: start.toISOString(),
|
||||
end_datetime: end.toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// For other periods, calculate from end of previous day
|
||||
const end = new Date(easternTime);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
|
||||
const start = new Date(easternTime);
|
||||
start.setDate(start.getDate() - Number(days));
|
||||
start.setHours(0, 0, 0, 0);
|
||||
|
||||
return {
|
||||
start_datetime: start.toISOString(),
|
||||
end_datetime: end.toISOString()
|
||||
};
|
||||
};
|
||||
|
||||
const MetricCard = ({
|
||||
title,
|
||||
value,
|
||||
delta,
|
||||
suffix = "",
|
||||
icon: Icon,
|
||||
colorClass = "blue",
|
||||
more_is_better = true,
|
||||
loading = false,
|
||||
}) => {
|
||||
const getDeltaColor = (d) => {
|
||||
if (d === 0) return "text-gray-600 dark:text-gray-400";
|
||||
const isPositive = d > 0;
|
||||
return isPositive === more_is_better
|
||||
? "text-green-600 dark:text-green-500"
|
||||
: "text-red-600 dark:text-red-500";
|
||||
};
|
||||
|
||||
const formatDelta = (d) => {
|
||||
if (d === undefined || d === null) return null;
|
||||
if (d === 0) return "0";
|
||||
return Math.abs(d) + suffix;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<CardContent className="pt-6 h-full">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-muted-foreground">{title}</p>
|
||||
{loading ? (
|
||||
<Skeleton className="h-8 w-24 dark:bg-gray-700" />
|
||||
) : (
|
||||
<div className="flex items-baseline gap-2">
|
||||
<p className="text-2xl font-bold">
|
||||
{typeof value === "number"
|
||||
? value.toLocaleString() + suffix
|
||||
: value}
|
||||
</p>
|
||||
{delta !== undefined && delta !== 0 && (
|
||||
<div className={`flex items-center ${getDeltaColor(delta)}`}>
|
||||
{delta > 0 ? (
|
||||
<ArrowUp className="w-3 h-3" />
|
||||
) : (
|
||||
<ArrowDown className="w-3 h-3" />
|
||||
)}
|
||||
<span className="text-xs font-medium">
|
||||
{formatDelta(delta)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{Icon && (
|
||||
<Icon className={`h-5 w-5 flex-shrink-0 ml-2 ${colorClass === "blue" ? "text-blue-500" :
|
||||
colorClass === "green" ? "text-green-500" :
|
||||
colorClass === "purple" ? "text-purple-500" :
|
||||
colorClass === "indigo" ? "text-indigo-500" :
|
||||
colorClass === "orange" ? "text-orange-500" :
|
||||
colorClass === "teal" ? "text-teal-500" :
|
||||
colorClass === "cyan" ? "text-cyan-500" :
|
||||
"text-blue-500"}`} />
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const TableSkeleton = () => (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-full dark:bg-gray-700" />
|
||||
<Skeleton className="h-8 w-full dark:bg-gray-700" />
|
||||
<Skeleton className="h-8 w-full dark:bg-gray-700" />
|
||||
<Skeleton className="h-8 w-full dark:bg-gray-700" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const GorgiasOverview = () => {
|
||||
const [timeRange, setTimeRange] = useState("7");
|
||||
const [data, setData] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const loadStats = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const filters = getDateRange(timeRange);
|
||||
|
||||
try {
|
||||
const [overview, channelStats, agentStats, satisfaction, selfService] =
|
||||
await Promise.all([
|
||||
axios.post('/api/gorgias/stats/overview', filters)
|
||||
.then(res => res.data?.data?.data?.data || []),
|
||||
axios.post('/api/gorgias/stats/tickets-created-per-channel', filters)
|
||||
.then(res => res.data?.data?.data?.data?.lines || []),
|
||||
axios.post('/api/gorgias/stats/tickets-closed-per-agent', filters)
|
||||
.then(res => res.data?.data?.data?.data?.lines || []),
|
||||
axios.post('/api/gorgias/stats/satisfaction-surveys', filters)
|
||||
.then(res => res.data?.data?.data?.data || []),
|
||||
axios.post('/api/gorgias/stats/self-service-overview', filters)
|
||||
.then(res => res.data?.data?.data?.data || []),
|
||||
]);
|
||||
|
||||
console.log('Raw API responses:', {
|
||||
overview,
|
||||
channelStats,
|
||||
agentStats,
|
||||
satisfaction,
|
||||
selfService
|
||||
});
|
||||
|
||||
setData({
|
||||
overview,
|
||||
channels: channelStats,
|
||||
agents: agentStats,
|
||||
satisfaction,
|
||||
selfService,
|
||||
});
|
||||
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error("Error loading stats:", err);
|
||||
const errorMessage = err.response?.data?.error || err.message;
|
||||
setError(errorMessage);
|
||||
if (err.response?.status === 401) {
|
||||
setError('Authentication failed. Please check your Gorgias API credentials.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [timeRange]);
|
||||
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
// Set up auto-refresh every 5 minutes
|
||||
const interval = setInterval(loadStats, 5 * 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [loadStats]);
|
||||
|
||||
// Convert overview array to stats format
|
||||
const stats = (data.overview || []).reduce((acc, item) => {
|
||||
acc[item.name] = {
|
||||
value: item.value || 0,
|
||||
delta: item.delta || 0,
|
||||
type: item.type,
|
||||
more_is_better: item.more_is_better
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
console.log('Processed stats:', stats);
|
||||
|
||||
// Process satisfaction data
|
||||
const satisfactionStats = (data.satisfaction || []).reduce((acc, item) => {
|
||||
if (item.name !== 'response_distribution') {
|
||||
acc[item.name] = {
|
||||
value: item.value || 0,
|
||||
delta: item.delta || 0,
|
||||
type: item.type,
|
||||
more_is_better: item.more_is_better
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
console.log('Processed satisfaction stats:', satisfactionStats);
|
||||
|
||||
// Process self-service data
|
||||
const selfServiceStats = (data.selfService || []).reduce((acc, item) => {
|
||||
acc[item.name] = {
|
||||
value: item.value || 0,
|
||||
delta: item.delta || 0,
|
||||
type: item.type,
|
||||
more_is_better: item.more_is_better
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
console.log('Processed self-service stats:', selfServiceStats);
|
||||
|
||||
// Process channel data
|
||||
const channels = data.channels?.map(line => ({
|
||||
name: line[0]?.value || '',
|
||||
total: line[1]?.value || 0,
|
||||
percentage: line[2]?.value || 0,
|
||||
delta: line[3]?.value || 0
|
||||
})) || [];
|
||||
|
||||
console.log('Processed channels:', channels);
|
||||
|
||||
// Process agent data
|
||||
const agents = data.agents?.map(line => ({
|
||||
name: line[0]?.value || '',
|
||||
closed: line[1]?.value || 0,
|
||||
rating: line[2]?.value,
|
||||
percentage: line[3]?.value || 0,
|
||||
delta: line[4]?.value || 0
|
||||
})) || [];
|
||||
|
||||
console.log('Processed agents:', agents);
|
||||
|
||||
if (error) return <p className="text-red-500">Error: {error}</p>;
|
||||
|
||||
return (
|
||||
<Card className="bg-white dark:bg-gray-900">
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">
|
||||
Customer Service
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={timeRange}
|
||||
onValueChange={(value) => setTimeRange(value)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px] bg-white dark:bg-gray-800">
|
||||
<SelectValue placeholder="Select range">
|
||||
{TIME_RANGES[timeRange]}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[
|
||||
["today", "Today"],
|
||||
["7", "Last 7 Days"],
|
||||
["14", "Last 14 Days"],
|
||||
["30", "Last 30 Days"],
|
||||
["90", "Last 90 Days"],
|
||||
].map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Message & Response Metrics */}
|
||||
<div className="h-full">
|
||||
<MetricCard
|
||||
title="Messages Received"
|
||||
value={stats.total_messages_received?.value}
|
||||
delta={stats.total_messages_received?.delta}
|
||||
icon={Mail}
|
||||
colorClass="blue"
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full">
|
||||
<MetricCard
|
||||
title="Messages Sent"
|
||||
value={stats.total_messages_sent?.value}
|
||||
delta={stats.total_messages_sent?.delta}
|
||||
icon={Send}
|
||||
colorClass="green"
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full">
|
||||
<MetricCard
|
||||
title="First Response"
|
||||
value={formatDuration(stats.median_first_response_time?.value)}
|
||||
delta={stats.median_first_response_time?.delta}
|
||||
icon={Zap}
|
||||
colorClass="purple"
|
||||
more_is_better={false}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full">
|
||||
<MetricCard
|
||||
title="One-Touch Rate"
|
||||
value={stats.total_one_touch_tickets?.value}
|
||||
delta={stats.total_one_touch_tickets?.delta}
|
||||
suffix="%"
|
||||
icon={BarChart3}
|
||||
colorClass="indigo"
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Satisfaction & Efficiency */}
|
||||
<div className="h-full">
|
||||
<MetricCard
|
||||
title="Customer Satisfaction"
|
||||
value={`${satisfactionStats.average_rating?.value}/5`}
|
||||
delta={satisfactionStats.average_rating?.delta}
|
||||
suffix="%"
|
||||
icon={Star}
|
||||
colorClass="orange"
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full">
|
||||
<MetricCard
|
||||
title="Survey Response Rate"
|
||||
value={satisfactionStats.response_rate?.value}
|
||||
delta={satisfactionStats.response_rate?.delta}
|
||||
suffix="%"
|
||||
icon={ClipboardCheck}
|
||||
colorClass="pink"
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full">
|
||||
<MetricCard
|
||||
title="Resolution Time"
|
||||
value={formatDuration(stats.median_resolution_time?.value)}
|
||||
delta={stats.median_resolution_time?.delta}
|
||||
icon={Timer}
|
||||
colorClass="teal"
|
||||
more_is_better={false}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full">
|
||||
<MetricCard
|
||||
title="Self-Service Rate"
|
||||
value={selfServiceStats.self_service_automation_rate?.value}
|
||||
delta={selfServiceStats.self_service_automation_rate?.delta}
|
||||
suffix="%"
|
||||
icon={Bot}
|
||||
colorClass="cyan"
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Channel Distribution */}
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg border dark:border-gray-800 p-4 pt-0">
|
||||
<div className="p-4 pl-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
Channel Distribution
|
||||
</h3>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="p-4">
|
||||
<TableSkeleton />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="dark:border-gray-800">
|
||||
<TableHead>Channel</TableHead>
|
||||
<TableHead className="text-right">Total</TableHead>
|
||||
<TableHead className="text-right">%</TableHead>
|
||||
<TableHead className="text-right">Change</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{channels
|
||||
.sort((a, b) => b.total - a.total)
|
||||
.map((channel, index) => (
|
||||
<TableRow key={index} className="dark:border-gray-800">
|
||||
<TableCell className="dark:text-gray-300">
|
||||
{channel.name}
|
||||
</TableCell>
|
||||
<TableCell className="text-right dark:text-gray-300">
|
||||
{channel.total}
|
||||
</TableCell>
|
||||
<TableCell className="text-right dark:text-gray-300">
|
||||
{channel.percentage}%
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={`text-right ${
|
||||
channel.delta > 0
|
||||
? "text-green-600 dark:text-green-500"
|
||||
: channel.delta < 0
|
||||
? "text-red-600 dark:text-red-500"
|
||||
: "dark:text-gray-300"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-end gap-0.5">
|
||||
{channel.delta !== 0 && (
|
||||
<>
|
||||
{channel.delta > 0 ? (
|
||||
<ArrowUp className="w-3 h-3" />
|
||||
) : (
|
||||
<ArrowDown className="w-3 h-3" />
|
||||
)}
|
||||
<span>{Math.abs(channel.delta)}%</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Agent Performance */}
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg border dark:border-gray-800 p-4 pt-0">
|
||||
<div className="p-4 pl-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
Agent Performance
|
||||
</h3>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="p-4">
|
||||
<TableSkeleton />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="dark:border-gray-800">
|
||||
<TableHead>Agent</TableHead>
|
||||
<TableHead className="text-right">Closed</TableHead>
|
||||
<TableHead className="text-right">Rating</TableHead>
|
||||
<TableHead className="text-right">Change</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{agents
|
||||
.filter((agent) => agent.name !== "Unassigned")
|
||||
.map((agent, index) => (
|
||||
<TableRow key={index} className="dark:border-gray-800">
|
||||
<TableCell className="dark:text-gray-300">
|
||||
{agent.name}
|
||||
</TableCell>
|
||||
<TableCell className="text-right dark:text-gray-300">
|
||||
{agent.closed}
|
||||
</TableCell>
|
||||
<TableCell className="text-right dark:text-gray-300">
|
||||
{agent.rating ? `${agent.rating}/5` : "-"}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={`text-right ${
|
||||
agent.delta > 0
|
||||
? "text-green-600 dark:text-green-500"
|
||||
: agent.delta < 0
|
||||
? "text-red-600 dark:text-red-500"
|
||||
: "dark:text-gray-300"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-end gap-0.5">
|
||||
{agent.delta !== 0 && (
|
||||
<>
|
||||
{agent.delta > 0 ? (
|
||||
<ArrowUp className="w-3 h-3" />
|
||||
) : (
|
||||
<ArrowDown className="w-3 h-3" />
|
||||
)}
|
||||
<span>{Math.abs(agent.delta)}%</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default GorgiasOverview;
|
||||
@@ -28,7 +28,8 @@ const Navigation = () => {
|
||||
{ id: "sales", label: "Sales Chart" },
|
||||
{ id: "campaigns", label: "Campaigns" },
|
||||
{ id: "meta-campaigns", label: "Meta Ads" },
|
||||
{ id: "calls", label: "Aircall" },
|
||||
{ id: "gorgias-overview", label: "Customer Service" },
|
||||
{ id: "calls", label: "Calls" },
|
||||
];
|
||||
|
||||
const sortSections = (sections) => {
|
||||
|
||||
15
dashboard/src/lib/api.js
Normal file
15
dashboard/src/lib/api.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// Create base64 encoded auth string
|
||||
const auth = Buffer.from(`${process.env.REACT_APP_GORGIAS_API_USERNAME}:${process.env.REACT_APP_GORGIAS_API_KEY}`).toString('base64');
|
||||
|
||||
// Create axios instance for Gorgias API
|
||||
const gorgiasApi = axios.create({
|
||||
baseURL: `https://${process.env.REACT_APP_GORGIAS_DOMAIN}.gorgias.com/api`,
|
||||
headers: {
|
||||
'Authorization': `Basic ${auth}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
export { gorgiasApi };
|
||||
@@ -167,6 +167,42 @@ export default defineConfig(({ mode }) => {
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
"/api/gorgias": {
|
||||
target: "https://dashboard.kent.pw",
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
rewrite: (path) => path.replace(/^\/api\/gorgias/, "/api/gorgias"),
|
||||
configure: (proxy, _options) => {
|
||||
proxy.on("error", (err, req, res) => {
|
||||
console.error("Gorgias proxy error:", err);
|
||||
res.writeHead(500, {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: "Proxy Error",
|
||||
message: err.message,
|
||||
details: err.stack
|
||||
})
|
||||
);
|
||||
});
|
||||
proxy.on("proxyReq", (proxyReq, req, _res) => {
|
||||
console.log("Outgoing Gorgias request:", {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
path: proxyReq.path,
|
||||
headers: proxyReq.getHeaders(),
|
||||
});
|
||||
});
|
||||
proxy.on("proxyRes", (proxyRes, req, _res) => {
|
||||
console.log("Gorgias proxy response:", {
|
||||
statusCode: proxyRes.statusCode,
|
||||
url: req.url,
|
||||
headers: proxyRes.headers,
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user