153 lines
4.8 KiB
TypeScript
153 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useTranslations } from "next-intl";
|
|
import { Card } from "@/components/ui/card";
|
|
import type {
|
|
SupportTicketCategory,
|
|
SupportTicketStatus,
|
|
} from "@/types";
|
|
|
|
const STATUSES: SupportTicketStatus[] = [
|
|
"open",
|
|
"in_progress",
|
|
"waiting_for_customer",
|
|
"resolved",
|
|
"reopened",
|
|
];
|
|
const CATEGORIES: SupportTicketCategory[] = [
|
|
"bug",
|
|
"feature_request",
|
|
"question",
|
|
"billing",
|
|
"other",
|
|
];
|
|
|
|
interface Props {
|
|
ticketId: string;
|
|
currentStatus: SupportTicketStatus;
|
|
currentCategory: SupportTicketCategory;
|
|
}
|
|
|
|
/**
|
|
* Admin-only controls — change ticket status / category. Visible
|
|
* exclusively when `user.isPlatform` (gate is in the parent server
|
|
* component, not here).
|
|
*
|
|
* Saves on dropdown change rather than via an explicit submit button
|
|
* — feels more like a queue-management panel than a form. Each save
|
|
* fires the email path (status change → status email to customer),
|
|
* so we deliberately don't auto-save category until the admin
|
|
* confirms; clicking through categories shouldn't spam status
|
|
* emails. (Status change emails the customer; category change does
|
|
* not — so category auto-save is fine. Status auto-save would also
|
|
* be fine in practice, but we keep an explicit save button on
|
|
* status to give admin a moment of pause before notifying.)
|
|
*
|
|
* In practice both fields auto-save — the email rule above is in
|
|
* the API anyway. If admin frustration with accidental status emails
|
|
* shows up in feedback, switch status to explicit-save.
|
|
*/
|
|
export function TicketAdminControls({
|
|
ticketId,
|
|
currentStatus,
|
|
currentCategory,
|
|
}: Props) {
|
|
const t = useTranslations("support");
|
|
const router = useRouter();
|
|
|
|
const [status, setStatus] = useState(currentStatus);
|
|
const [category, setCategory] = useState(currentCategory);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState("");
|
|
|
|
const saveChange = async (changes: {
|
|
status?: SupportTicketStatus;
|
|
category?: SupportTicketCategory;
|
|
}) => {
|
|
setSaving(true);
|
|
setError("");
|
|
try {
|
|
const res = await fetch(
|
|
`/api/support/tickets/${encodeURIComponent(ticketId)}`,
|
|
{
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(changes),
|
|
}
|
|
);
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}));
|
|
throw new Error(data.error || t("updateFailed"));
|
|
}
|
|
router.refresh();
|
|
} catch (e: any) {
|
|
setError(e.message);
|
|
// Revert local state on failure so the UI doesn't lie about
|
|
// what's saved.
|
|
if (changes.status) setStatus(currentStatus);
|
|
if (changes.category) setCategory(currentCategory);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card className="border-blue-400/30 bg-blue-400/5">
|
|
<div className="text-xs uppercase tracking-wider text-blue-400 font-semibold mb-3">
|
|
{t("adminControlsTitle")}
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs uppercase tracking-wider text-text-muted mb-1">
|
|
{t("fieldStatus")}
|
|
</label>
|
|
<select
|
|
value={status}
|
|
disabled={saving}
|
|
onChange={(e) => {
|
|
const next = e.target.value as SupportTicketStatus;
|
|
setStatus(next);
|
|
saveChange({ status: next });
|
|
}}
|
|
className="w-full px-3 py-2 rounded-lg border border-border bg-surface-2 text-text-primary text-sm focus:outline-none focus:border-text-secondary disabled:opacity-50"
|
|
>
|
|
{STATUSES.map((s) => (
|
|
<option key={s} value={s}>
|
|
{t(`status_${s}`)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs uppercase tracking-wider text-text-muted mb-1">
|
|
{t("fieldCategory")}
|
|
</label>
|
|
<select
|
|
value={category}
|
|
disabled={saving}
|
|
onChange={(e) => {
|
|
const next = e.target.value as SupportTicketCategory;
|
|
setCategory(next);
|
|
saveChange({ category: next });
|
|
}}
|
|
className="w-full px-3 py-2 rounded-lg border border-border bg-surface-2 text-text-primary text-sm focus:outline-none focus:border-text-secondary disabled:opacity-50"
|
|
>
|
|
{CATEGORIES.map((c) => (
|
|
<option key={c} value={c}>
|
|
{t(`category_${c}`)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
{error && (
|
|
<div className="text-xs text-red-400 bg-red-400/10 border border-red-400/20 rounded-lg px-3 py-2 mt-3">
|
|
{error}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|