feat(CC-084): add Professional Identity settings (bio/role/links/locale via workspace PATCH)

This commit is contained in:
admin-valentin 2026-08-02 17:31:34 +00:00
parent 3a9245b186
commit a25c54970f

View file

@ -0,0 +1,152 @@
'use client';
import { useMemo, useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiFetch } from '../../../../lib/api';
import { useSession } from '../../../../components/session-provider';
interface Workspace {
id: string; name: string; slug: string | null; settings: Record<string, unknown> | null;
logoUrl: string | null; timezone: string | null; locale: string | null; createdAt: string;
}
const TIMEZONES = [
'Europe/Bucharest', 'Europe/Berlin', 'Europe/London', 'Europe/Paris',
'America/New_York', 'America/Chicago', 'America/Los_Angeles',
'Asia/Kolkata', 'Asia/Tokyo', 'UTC',
];
const LOCALES = [
{ value: 'ro-RO', label: 'Română (RO)' },
{ value: 'de-DE', label: 'Deutsch (DE)' },
{ value: 'en-US', label: 'English (US)' },
{ value: 'es-ES', label: 'Español (ES)' },
];
export default function SettingsIdentityPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const qc = useQueryClient();
const { data: ws, isLoading } = useQuery({
queryKey: ['ws-identity', tenantId],
queryFn: () => apiFetch<Workspace>('/v1/workspace', { tenantId }),
enabled: Boolean(tenantId), staleTime: 60_000,
});
const [form, setForm] = useState({ name: '', slug: '', timezone: '', locale: '', bio: '', website: '', linkedin: '', role: '' });
const [saved, setSaved] = useState(false);
useEffect(() => {
if (ws) {
const s = (ws.settings ?? {}) as Record<string, string>;
setForm({
name: ws.name ?? '',
slug: ws.slug ?? '',
timezone: ws.timezone ?? 'Europe/Bucharest',
locale: ws.locale ?? 'ro-RO',
bio: s.bio ?? '',
website: s.website ?? '',
linkedin: s.linkedin ?? '',
role: s.role ?? '',
});
}
}, [ws]);
const saveMut = useMutation({
mutationFn: () => apiFetch('/v1/workspace', { tenantId, method: 'PATCH', body: {
name: form.name, slug: form.slug || undefined,
timezone: form.timezone, locale: form.locale,
settings: { bio: form.bio, website: form.website, linkedin: form.linkedin, role: form.role },
}}),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['ws-identity', tenantId] }); setSaved(true); setTimeout(() => setSaved(false), 3000); },
});
if (isLoading) return <div className="p-6 text-sm text-ink-faint">Se încarcă</div>;
return (
<div className="max-w-2xl space-y-6 p-6">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Identitate Profesională</h1>
<p className="text-sm text-ink-faint mt-1">Profilul și setările de bază ale workspace-ului CEO OS.</p>
</div>
{/* Identity */}
<div className="card p-5 space-y-4">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Informații de bază</p>
<div className="space-y-3">
<div className="space-y-1">
<label className="text-xs font-medium text-ink-faint">Nume workspace / Nume profesional</label>
<input value={form.name} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))}
className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-ink-faint">Slug URL (identificator unic)</label>
<input value={form.slug} onChange={(e) => setForm((p) => ({ ...p, slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '') }))}
placeholder="ex: valentin-ion"
className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink font-mono focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-ink-faint">Rol / Titlu profesional</label>
<input value={form.role} onChange={(e) => setForm((p) => ({ ...p, role: e.target.value }))}
placeholder="ex: CTO & AI Strategist"
className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-ink-faint">Bio scurt</label>
<textarea value={form.bio} onChange={(e) => setForm((p) => ({ ...p, bio: e.target.value }))}
rows={3} placeholder="Câteva propoziții despre tine și activitatea ta…"
className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring resize-none" />
</div>
</div>
</div>
{/* Links */}
<div className="card p-5 space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Link-uri publice</p>
<div className="space-y-1">
<label className="text-xs font-medium text-ink-faint">Website personal</label>
<input value={form.website} onChange={(e) => setForm((p) => ({ ...p, website: e.target.value }))}
placeholder="https://example.com"
className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink font-mono focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-ink-faint">LinkedIn URL</label>
<input value={form.linkedin} onChange={(e) => setForm((p) => ({ ...p, linkedin: e.target.value }))}
placeholder="https://linkedin.com/in/username"
className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink font-mono focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
</div>
{/* Locale */}
<div className="card p-5 space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Localizare</p>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1">
<label className="text-xs font-medium text-ink-faint">Fus orar</label>
<select value={form.timezone} onChange={(e) => setForm((p) => ({ ...p, timezone: e.target.value }))}
className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring">
{TIMEZONES.map((tz) => <option key={tz} value={tz}>{tz}</option>)}
</select>
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-ink-faint">Limbă & format</label>
<select value={form.locale} onChange={(e) => setForm((p) => ({ ...p, locale: e.target.value }))}
className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring">
{LOCALES.map((l) => <option key={l.value} value={l.value}>{l.label}</option>)}
</select>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<button onClick={() => saveMut.mutate()} disabled={saveMut.isPending || !form.name}
className="rounded-lg bg-primary px-6 py-2 text-sm font-medium text-white hover:bg-primary/90 disabled:opacity-50">
{saveMut.isPending ? 'Se salvează…' : 'Salvează modificările'}
</button>
{saved && <span className="text-sm text-signal-ok"> Salvat</span>}
{saveMut.isError && <span className="text-sm text-signal-danger">Eroare la salvare</span>}
</div>
</div>
);
}