Files
TicketingSystem/client/src/pages/Settings.tsx
T
josh d8785a964d
Build & Push / Test (client) (push) Successful in 31s
Build & Push / Test (server) (push) Successful in 38s
Build & Push / Build Client (push) Successful in 1m17s
Build & Push / Build Server (push) Successful in 1m18s
Merge SERVICE role into AGENT
Every AGENT now gets an auto-generated API key on creation, shown once
in a modal. AGENTs log in with password and authenticate to the API
with X-Api-Key. pre-push.sql defensively migrates any residual SERVICE
rows to AGENT before Prisma rewrites the enum. Goddard is no longer
baked into the seed — create agents via Admin → Users.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-18 22:44:32 -04:00

127 lines
4.3 KiB
TypeScript

import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import Layout from '../components/Layout';
import { useAuth } from '../contexts/AuthContext';
import {
useNotificationPrefs,
useUpdateNotificationPrefs,
type NotificationPrefs,
} from '../api/queries';
const CHANNELS = [
{ key: 'assignment', label: 'Ticket assigned to me' },
{ key: 'mention', label: 'I am mentioned in a comment' },
{ key: 'resolved', label: 'A ticket I created is resolved' },
] as const;
export default function Settings() {
const { user } = useAuth();
const prefsQ = useNotificationPrefs();
const updatePrefs = useUpdateNotificationPrefs();
const [local, setLocal] = useState<NotificationPrefs | null>(null);
useEffect(() => {
if (prefsQ.data && !local) setLocal(prefsQ.data);
}, [prefsQ.data, local]);
const handleToggle = (channel: 'email' | 'inApp', key: keyof NotificationPrefs['email']) => {
setLocal((l) =>
l
? {
...l,
[channel]: { ...l[channel], [key]: !l[channel][key] },
}
: l,
);
};
const save = async () => {
if (!local) return;
try {
await updatePrefs.mutateAsync(local);
toast.success('Preferences saved');
} catch (e) {
toast.error((e as Error).message || 'Failed to save');
}
};
return (
<Layout title="Settings">
<div className="space-y-6">
{/* Profile */}
<section className="rounded-md border border-border p-4">
<h2 className="text-sm font-semibold mb-3">Profile</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<Field label="Display name" value={user?.displayName ?? ''} />
<Field label="Username" value={user?.username ?? ''} mono />
<Field label="Email" value={user?.email ?? ''} />
<Field label="Role" value={user?.role ?? ''} />
</div>
</section>
{/* Notifications */}
<section className="rounded-md border border-border p-4">
<h2 className="text-sm font-semibold mb-3">Notification preferences</h2>
{local ? (
<div className="space-y-3">
<div className="grid grid-cols-[1fr_80px_80px] gap-2 text-xs uppercase text-muted-foreground pb-1 border-b border-border">
<span />
<span className="text-center">Email</span>
<span className="text-center">In app</span>
</div>
{CHANNELS.map(({ key, label }) => (
<div
key={key}
className="grid grid-cols-[1fr_80px_80px] gap-2 items-center text-sm"
>
<span>{label}</span>
<div className="text-center">
<input
type="checkbox"
checked={local.email[key]}
onChange={() => handleToggle('email', key)}
/>
</div>
<div className="text-center">
<input
type="checkbox"
checked={local.inApp[key]}
onChange={() => handleToggle('inApp', key)}
/>
</div>
</div>
))}
<div className="flex justify-end pt-2">
<button
onClick={save}
disabled={updatePrefs.isPending}
className="px-3 py-1.5 bg-primary text-primary-foreground rounded-md text-sm disabled:opacity-50"
>
{updatePrefs.isPending ? 'Saving…' : 'Save preferences'}
</button>
</div>
</div>
) : (
<p className="text-sm text-muted-foreground">Loading</p>
)}
<p className="mt-3 text-xs text-muted-foreground">
Email notifications require the server&apos;s SMTP config. If SMTP is unset, only
in-app delivery happens regardless of these settings.
</p>
</section>
</div>
</Layout>
);
}
function Field({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
return (
<div>
<p className="text-xs text-muted-foreground mb-0.5">{label}</p>
<p className={`text-sm ${mono ? 'font-mono' : ''}`}>{value || '—'}</p>
</div>
);
}