d8785a964d
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>
127 lines
4.3 KiB
TypeScript
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'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>
|
|
);
|
|
}
|