diff --git a/packages/archioffice-agents/src/client/AgentConfig.tsx b/packages/archioffice-agents/src/client/AgentConfig.tsx index eb2925a..b13985b 100644 --- a/packages/archioffice-agents/src/client/AgentConfig.tsx +++ b/packages/archioffice-agents/src/client/AgentConfig.tsx @@ -3,7 +3,8 @@ import { useParams, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { IconArrowLeft, IconRobot, IconChevronDown } from '@tabler/icons-react'; import { apiFetch } from '@/src/lib/api'; -import type { Agent, AgentContextScope } from '../types.js'; +import type { Agent, AgentContextScope, AgentActionScope } from '../types.js'; +import { AGENT_RESOURCES } from '../types.js'; const AVATAR_COLORS = ['#206bc4', '#2fb344', '#f76707', '#ae3ec9', '#1098ad', '#e8590c']; @@ -15,6 +16,10 @@ const SCOPES: { key: AgentContextScope; label: string }[] = [ { key: 'tasks', label: 'Tâches' }, ]; +// One toggle per resource grants that resource's full create/update/delete +// surface — the same surface a human has in that section of the app. +const ALL_ACTION_KEYS: AgentActionScope[] = AGENT_RESOURCES.map(r => r.key); + export default function AgentConfig() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); @@ -33,6 +38,7 @@ export default function AgentConfig() { const [tone, setTone] = useState(''); const [directives, setDirectives] = useState(''); const [contextScopes, setContextScopes] = useState([]); + const [actionScopes, setActionScopes] = useState([]); const [systemPromptOverride, setSystemPromptOverride] = useState(''); useEffect(() => { @@ -48,6 +54,7 @@ export default function AgentConfig() { setTone(found.tone ?? ''); setDirectives(found.directives ?? ''); setContextScopes(found.context_scopes as AgentContextScope[]); + setActionScopes((found.action_scopes ?? []) as AgentActionScope[]); setSystemPromptOverride(found.system_prompt_override ?? ''); }) .finally(() => setLoading(false)); @@ -59,6 +66,17 @@ export default function AgentConfig() { ); }; + const toggleActionScope = (scope: AgentActionScope) => { + setActionScopes((prev: AgentActionScope[]) => + prev.includes(scope) ? prev.filter((s: AgentActionScope) => s !== scope) : [...prev, scope] + ); + }; + + const allActionsSelected = ALL_ACTION_KEYS.every(k => actionScopes.includes(k)); + const toggleAllActionScopes = () => { + setActionScopes(allActionsSelected ? [] : [...ALL_ACTION_KEYS]); + }; + const handleSave = async () => { if (!agent) return; setSaving(true); @@ -69,6 +87,7 @@ export default function AgentConfig() { name, role_title: roleTitle, avatar_initials: avatarInitials, avatar_color: avatarColor, tone, directives, context_scopes: contextScopes, + action_scopes: actionScopes, system_prompt_override: systemPromptOverride || null, }), }); @@ -191,6 +210,35 @@ export default function AgentConfig() { + {/* Actions — write permissions */} +
+
+

{t('agent_config_actions')}

+ +
+

{t('agent_config_actions_hint')}

+
+ {AGENT_RESOURCES.map(scope => ( + + ))} +
+
+ {/* Advanced prompt */}
+ {error && ( +
+ + {error} +
+ )}
{/* Identité Section */}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 6d479f6..ded0d8d 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -29,7 +29,7 @@ import { IconCloudUpload, IconBuildingBank, } from '@tabler/icons-react'; -import { ArchiOfficeLogo } from './ArchiOfficeLogo'; +import { BrandLogo } from './ArchiOfficeLogo'; import { useUser } from '../UserContext'; import { useSettings } from '../hooks/useSettings'; import { apiFetch } from '../lib/api'; @@ -172,7 +172,7 @@ export function Sidebar() { className="flex items-center gap-2 font-bold text-base tracking-tight" style={{ color: 'var(--tblr-text)' }} > - + ArchiOffice {settings?.agencyName && ( diff --git a/src/locales/fr.ts b/src/locales/fr.ts index cc8fc35..5524034 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -663,6 +663,10 @@ export default { "agent_config_identity": "Identité", "agent_config_directives": "Directives", "agent_config_scopes": "Accès aux données", + "agent_config_actions": "Actions autorisées", + "agent_config_actions_hint": "Chaque ressource cochée donne à l'agent le droit de créer, modifier et supprimer des données à cet endroit, exactement comme un humain avec accès à cette section. À activer uniquement pour les agents de confiance.", + "agent_config_actions_select_all": "Tout activer", + "agent_config_actions_clear_all": "Tout désactiver", "agent_config_prompt": "Prompt avancé", "agent_config_prompt_hint": "Laisser vide pour utiliser le prompt généré automatiquement", "agent_config_save": "Enregistrer", diff --git a/src/pages/Contacts.tsx b/src/pages/Contacts.tsx index 8f39765..c72a98d 100644 --- a/src/pages/Contacts.tsx +++ b/src/pages/Contacts.tsx @@ -169,30 +169,16 @@ export default function Contacts() { const url = isEditing ? `/api/contacts/${editingId}` : '/api/contacts'; const method = isEditing ? 'PUT' : 'POST'; - const res = await fetch(url, { - method, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(contact) - }); + await apiFetch(url, { method, body: JSON.stringify(contact) }); - if (res.ok) { - setIsModalOpen(false); - setIsEditing(false); - setEditingId(null); - setNewContact({}); - fetchContacts(); - } else { - const text = await res.text(); - try { - const errorData = JSON.parse(text); - console.error('Failed to save contact:', errorData); - alert(`Failed to save contact: ${errorData.error || 'Unknown error'}`); - } catch (e) { - console.error('Failed to save contact (non-JSON response):', text); - alert(`Failed to save contact: Server returned ${res.status} ${res.statusText}`); - } - } + setIsModalOpen(false); + setIsEditing(false); + setEditingId(null); + setNewContact({}); + fetchContacts(); + showToast(isEditing ? 'Contact modifié' : 'Contact créé'); } catch (err: any) { + console.error('Failed to save contact:', err); showToast(err?.message || 'Erreur lors de la sauvegarde du contact', 'error'); } }; @@ -315,6 +301,7 @@ export default function Contacts() { const handleConfirm = async () => { if (!importData) return; + let failed = 0; for (const row of importData.data) { const contact: Contact = { ...defaultContact, @@ -338,15 +325,20 @@ export default function Contacts() { contact.country = contact.address_work_country || ''; contact.ca_amount = Number(contact.ca_amount) || 0; - await fetch('/api/contacts', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(contact) - }); + try { + await apiFetch('/api/contacts', { method: 'POST', body: JSON.stringify(contact) }); + } catch (err) { + console.error('Failed to import contact:', err); + failed++; + } } fetchContacts(); setIsMappingModalOpen(false); setImportData(null); + showToast( + failed > 0 ? `Import terminé avec ${failed} échec(s)` : 'Import terminé', + failed > 0 ? 'error' : 'success' + ); }; if (!isMappingModalOpen || !importData) return null; diff --git a/supabase/migrate_add_agent_actions.sql b/supabase/migrate_add_agent_actions.sql new file mode 100644 index 0000000..39114f6 --- /dev/null +++ b/supabase/migrate_add_agent_actions.sql @@ -0,0 +1,9 @@ +-- ============================================================ +-- ArchiOffice — Migration : Actions d'écriture pour les Agents IA +-- ============================================================ +-- Les agents ne pouvaient jusqu'ici que lire les données du cabinet +-- (context_scopes). Cette colonne leur donne, agent par agent et sur +-- décision explicite de l'architecte, la permission de créer des +-- données (contacts, devis) via des function calls Gemini. + +ALTER TABLE agents ADD COLUMN IF NOT EXISTS action_scopes TEXT[] NOT NULL DEFAULT '{}'; diff --git a/supabase/migrate_add_team_members_auth_user_id.sql b/supabase/migrate_add_team_members_auth_user_id.sql new file mode 100644 index 0000000..6586e95 --- /dev/null +++ b/supabase/migrate_add_team_members_auth_user_id.sql @@ -0,0 +1,17 @@ +-- ============================================================ +-- ArchiOffice — Migration : team_members.auth_user_id +-- ============================================================ +-- server.ts looks up the current user's team_members row (display name, +-- notifications_last_seen) by their Supabase Auth id. That lookup was +-- filtering on a `user_id` column that was never actually created by any +-- committed migration — team_members.id is the row's own primary key, not +-- the auth id, so every call to getUserName()/the notifications endpoints +-- failed with "column team_members.user_id does not exist" (visible in +-- production logs on every activity-logging call, e.g. right after +-- creating a contact — the insert itself succeeded, but the follow-up +-- getUserName() call then threw, turning the whole request into a 500). +-- This column formalizes the auth-id link so lookups succeed. + +ALTER TABLE team_members ADD COLUMN IF NOT EXISTS auth_user_id TEXT; + +CREATE INDEX IF NOT EXISTS idx_team_members_auth_user ON team_members(auth_user_id, tenant_id);