Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,14 @@ NEXTAUTH_SECRET=your-super-secret-key-here-generate-with-openssl-rand-base64-32
# S3_REGION=us-east-1
# S3_ACCESS_KEY=your_access_key
# S3_SECRET_KEY=your_secret_key

# ==============================================================================
# Freshdesk Integration (Optional - for IT Tickets)
# Configure via Admin Settings UI or environment variables
# ==============================================================================

# Freshdesk domain (e.g., "yourcompany" for yourcompany.freshdesk.com)
# FRESHDESK_DOMAIN=yourcompany

# Freshdesk API key (found in Profile Settings > API Key)
# FRESHDESK_API_KEY=your_freshdesk_api_key
12 changes: 11 additions & 1 deletion src/app/admin/settings/ui/AdminSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
FileText,
Bell,
Shield,
Ticket,
} from "lucide-react";
import EmailSettingsTab from "./EmailSettingsTab";
import UsersSettingsTab from "./UsersSettingsTab";
Expand All @@ -20,6 +21,7 @@ import DepreciationSettingsTab from "./DepreciationSettingsTab";
import CustomFieldsTab from "./CustomFieldsTab";
import NotificationSettingsTab from "./NotificationSettingsTab";
import GeneralSettingsTab from "./GeneralSettingsTab";
import FreshdeskSettingsTab from "./FreshdeskSettingsTab";

interface AdminSettingsPageProps {
settings: Record<
Expand Down Expand Up @@ -101,7 +103,7 @@ export default function AdminSettingsPage({
<Separator />

<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-2 h-auto p-1">
<TabsList className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-2 h-auto p-1">
<TabsTrigger value="general" className="flex items-center gap-2">
<Settings className="h-4 w-4" />
<span className="hidden sm:inline">General</span>
Expand All @@ -110,6 +112,10 @@ export default function AdminSettingsPage({
<Mail className="h-4 w-4" />
<span className="hidden sm:inline">Email</span>
</TabsTrigger>
<TabsTrigger value="freshdesk" className="flex items-center gap-2">
<Ticket className="h-4 w-4" />
<span className="hidden sm:inline">Freshdesk</span>
</TabsTrigger>
<TabsTrigger value="notifications" className="flex items-center gap-2">
<Bell className="h-4 w-4" />
<span className="hidden sm:inline">Notifications</span>
Expand Down Expand Up @@ -141,6 +147,10 @@ export default function AdminSettingsPage({
<EmailSettingsTab settings={settings.email || []} />
</TabsContent>

<TabsContent value="freshdesk">
<FreshdeskSettingsTab settings={settings.freshdesk || []} />
</TabsContent>

<TabsContent value="notifications">
<NotificationSettingsTab settings={settings.notifications || []} />
</TabsContent>
Expand Down
283 changes: 283 additions & 0 deletions src/app/admin/settings/ui/FreshdeskSettingsTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
"use client";

import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import {
CheckCircle,
XCircle,
Loader2,
Eye,
EyeOff,
ExternalLink,
Ticket,
} from "lucide-react";

interface FreshdeskSettingsTabProps {
settings: Array<{
id: string;
key: string;
value: string | null;
type: string;
description: string | null;
isEncrypted: boolean;
}>;
}

export default function FreshdeskSettingsTab({ settings }: FreshdeskSettingsTabProps) {
const getSettingValue = (key: string) =>
settings.find((s) => s.key === key)?.value || "";

const [domain, setDomain] = useState(getSettingValue("freshdesk_domain") || "");
const [apiKey, setApiKey] = useState(
getSettingValue("freshdesk_api_key") ? "********" : ""
);
const [showApiKey, setShowApiKey] = useState(false);
const [isTesting, setIsTesting] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [connectionStatus, setConnectionStatus] = useState<
"unknown" | "connected" | "failed"
>("unknown");

const handleSave = async () => {
if (!domain) {
toast.error("Freshdesk domain is required");
return;
}

setIsSaving(true);
try {
const response = await fetch("/api/admin/settings/freshdesk", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
domain,
apiKey: apiKey !== "********" ? apiKey : undefined,
}),
});

if (response.ok) {
toast.success("Freshdesk settings saved successfully");
// Mark API key as saved
if (apiKey && apiKey !== "********") {
setApiKey("********");
}
} else {
const error = await response.json();
toast.error(error.error || "Failed to save settings");
}
} catch {
toast.error("Failed to save settings");
} finally {
setIsSaving(false);
}
};

const handleTestConnection = async () => {
if (!domain) {
toast.error("Please enter your Freshdesk domain first");
return;
}

setIsTesting(true);
setConnectionStatus("unknown");
try {
const response = await fetch("/api/admin/settings/freshdesk/test", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
domain,
apiKey: apiKey !== "********" ? apiKey : "********",
}),
});

const result = await response.json();
if (result.success) {
setConnectionStatus("connected");
toast.success("Successfully connected to Freshdesk!");
} else {
setConnectionStatus("failed");
toast.error(result.error || "Connection test failed");
}
} catch {
setConnectionStatus("failed");
toast.error("Connection test failed");
} finally {
setIsTesting(false);
}
};

return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Ticket className="h-5 w-5" />
Freshdesk Integration
</span>
{connectionStatus === "connected" && (
<Badge
variant="outline"
className="bg-green-50 text-green-700 border-green-200"
>
<CheckCircle className="h-3 w-3 mr-1" />
Connected
</Badge>
)}
{connectionStatus === "failed" && (
<Badge
variant="outline"
className="bg-red-50 text-red-700 border-red-200"
>
<XCircle className="h-3 w-3 mr-1" />
Connection Failed
</Badge>
)}
</CardTitle>
<CardDescription>
Connect to Freshdesk to view IT support tickets for Hardware Requests
and Problems
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-4">
<div className="space-y-2">
<Label htmlFor="domain">Freshdesk Domain</Label>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">https://</span>
<Input
id="domain"
value={domain}
onChange={(e) => setDomain(e.target.value)}
placeholder="yourcompany"
className="flex-1"
/>
<span className="text-sm text-muted-foreground">
.freshdesk.com
</span>
</div>
<p className="text-sm text-muted-foreground">
Enter your Freshdesk subdomain (e.g., &quot;yourcompany&quot; from
yourcompany.freshdesk.com)
</p>
</div>

<div className="space-y-2">
<Label htmlFor="apiKey">API Key</Label>
<div className="relative">
<Input
id="apiKey"
type={showApiKey ? "text" : "password"}
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="Enter your Freshdesk API key"
className="pr-10"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3"
onClick={() => setShowApiKey(!showApiKey)}
>
{showApiKey ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<p className="text-sm text-muted-foreground">
Find your API key in Freshdesk under Profile Settings &gt; API Key
</p>
</div>
</div>

<div className="flex justify-between">
<Button
variant="outline"
onClick={handleTestConnection}
// Disable if testing, no domain, or no API key (unless it's masked from database)
disabled={isTesting || !domain || (!apiKey && apiKey !== "********")}
>
{isTesting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Test Connection
</Button>
<Button
onClick={handleSave}
// Disable if saving, no domain, or no API key (unless it's masked from database)
disabled={isSaving || !domain || (!apiKey && apiKey !== "********")}
>
{isSaving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Save Settings
</Button>
</div>
</CardContent>
</Card>

<Card>
<CardHeader>
<CardTitle>Ticket Types</CardTitle>
<CardDescription>
The following Freshdesk ticket types will be synced and displayed in
your IT Tickets section
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">Hardware Request</Badge>
<Badge variant="secondary">Problem</Badge>
</div>
<p className="text-sm text-muted-foreground mt-4">
Tickets with these types will appear in the IT Tickets page. Make sure
these ticket types are configured in your Freshdesk admin settings.
</p>
</CardContent>
</Card>

<Card>
<CardHeader>
<CardTitle>Setup Guide</CardTitle>
<CardDescription>How to get your Freshdesk API key</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<ol className="list-decimal list-inside space-y-2 text-sm">
<li>Log in to your Freshdesk account</li>
<li>Click on your profile picture in the top right</li>
<li>Select &quot;Profile Settings&quot;</li>
<li>Scroll down to find your API Key on the right side</li>
<li>Copy the API key and paste it above</li>
</ol>
<Alert>
<AlertDescription>
Your API key is stored securely and encrypted. It will only be used
to fetch ticket data from Freshdesk.
</AlertDescription>
</Alert>
<Button variant="link" className="p-0" asChild>
<a
href="https://support.freshdesk.com/support/solutions/articles/215517-how-to-find-your-api-key"
target="_blank"
rel="noopener noreferrer"
>
Learn more about Freshdesk API keys
<ExternalLink className="h-3 w-3 ml-1" />
</a>
</Button>
</CardContent>
</Card>
</div>
);
}
Loading