@tleblancureta/proto 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core-web/src/ProtoApp.tsx +30 -20
- package/core-web/src/components/Shell.tsx +2 -2
- package/core-web/src/components/admin/AdminPanel.tsx +285 -0
- package/core-web/src/components/admin/SystemTab.tsx +167 -0
- package/core-web/src/components/shell/EmptyState.tsx +1 -1
- package/core-web/src/components/shell/FocusView.tsx +1 -1
- package/core-web/src/components/shell/Toolbar.tsx +14 -4
- package/core-web/src/components/ui/avatar.tsx +1 -1
- package/core-web/src/components/ui/badge.tsx +1 -1
- package/core-web/src/components/ui/button.tsx +1 -1
- package/core-web/src/components/ui/card.tsx +1 -1
- package/core-web/src/components/ui/input.tsx +1 -1
- package/core-web/src/components/ui/scroll-area.tsx +1 -1
- package/core-web/src/components/ui/separator.tsx +1 -1
- package/core-web/src/components/ui/shell-dialog.tsx +1 -1
- package/core-web/src/components/ui/skeleton.tsx +1 -1
- package/core-web/src/components/ui/textarea.tsx +1 -1
- package/core-web/src/components/widgets/agent/Primitives.tsx +1 -1
- package/core-web/src/components/widgets/agent/actions.ts +1 -1
- package/core-web/src/hooks/useAuth.ts +1 -1
- package/core-web/src/index.ts +1 -0
- package/core-web/src/lib/define-widget.ts +1 -1
- package/dist/core-web/src/ProtoApp.d.ts.map +1 -1
- package/dist/core-web/src/ProtoApp.js +5 -3
- package/dist/core-web/src/ProtoApp.js.map +1 -1
- package/dist/core-web/src/components/Shell.d.ts +1 -1
- package/dist/core-web/src/components/Shell.d.ts.map +1 -1
- package/dist/core-web/src/components/Shell.js +1 -1
- package/dist/core-web/src/components/Shell.js.map +1 -1
- package/dist/core-web/src/components/admin/AdminPanel.d.ts +7 -0
- package/dist/core-web/src/components/admin/AdminPanel.d.ts.map +1 -0
- package/dist/core-web/src/components/admin/AdminPanel.js +112 -0
- package/dist/core-web/src/components/admin/AdminPanel.js.map +1 -0
- package/dist/core-web/src/components/admin/SystemTab.d.ts +2 -0
- package/dist/core-web/src/components/admin/SystemTab.d.ts.map +1 -0
- package/dist/core-web/src/components/admin/SystemTab.js +25 -0
- package/dist/core-web/src/components/admin/SystemTab.js.map +1 -0
- package/dist/core-web/src/components/shell/EmptyState.js +1 -1
- package/dist/core-web/src/components/shell/EmptyState.js.map +1 -1
- package/dist/core-web/src/components/shell/FocusView.js +1 -1
- package/dist/core-web/src/components/shell/FocusView.js.map +1 -1
- package/dist/core-web/src/components/shell/Toolbar.d.ts.map +1 -1
- package/dist/core-web/src/components/shell/Toolbar.js +7 -5
- package/dist/core-web/src/components/shell/Toolbar.js.map +1 -1
- package/dist/core-web/src/components/ui/avatar.js +1 -1
- package/dist/core-web/src/components/ui/avatar.js.map +1 -1
- package/dist/core-web/src/components/ui/badge.js +1 -1
- package/dist/core-web/src/components/ui/badge.js.map +1 -1
- package/dist/core-web/src/components/ui/button.js +1 -1
- package/dist/core-web/src/components/ui/button.js.map +1 -1
- package/dist/core-web/src/components/ui/card.js +1 -1
- package/dist/core-web/src/components/ui/card.js.map +1 -1
- package/dist/core-web/src/components/ui/input.js +1 -1
- package/dist/core-web/src/components/ui/input.js.map +1 -1
- package/dist/core-web/src/components/ui/scroll-area.js +1 -1
- package/dist/core-web/src/components/ui/scroll-area.js.map +1 -1
- package/dist/core-web/src/components/ui/separator.js +1 -1
- package/dist/core-web/src/components/ui/separator.js.map +1 -1
- package/dist/core-web/src/components/ui/shell-dialog.js +1 -1
- package/dist/core-web/src/components/ui/shell-dialog.js.map +1 -1
- package/dist/core-web/src/components/ui/skeleton.js +1 -1
- package/dist/core-web/src/components/ui/skeleton.js.map +1 -1
- package/dist/core-web/src/components/ui/textarea.js +1 -1
- package/dist/core-web/src/components/ui/textarea.js.map +1 -1
- package/dist/core-web/src/components/widgets/agent/Primitives.js +1 -1
- package/dist/core-web/src/components/widgets/agent/Primitives.js.map +1 -1
- package/dist/core-web/src/components/widgets/agent/actions.js +1 -1
- package/dist/core-web/src/components/widgets/agent/actions.js.map +1 -1
- package/dist/core-web/src/hooks/useAuth.js +1 -1
- package/dist/core-web/src/hooks/useAuth.js.map +1 -1
- package/dist/core-web/src/index.d.ts +1 -0
- package/dist/core-web/src/index.d.ts.map +1 -1
- package/dist/core-web/src/index.js +1 -0
- package/dist/core-web/src/index.js.map +1 -1
- package/dist/core-web/src/lib/define-widget.d.ts +1 -1
- package/dist/core-web/src/lib/define-widget.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
* }
|
|
19
19
|
*/
|
|
20
20
|
import { useState, useCallback, useRef, useMemo } from 'react'
|
|
21
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom'
|
|
21
22
|
import Shell, { type CockpitDefinition } from './components/Shell.js'
|
|
23
|
+
import { AdminPanel } from './components/admin/AdminPanel.js'
|
|
22
24
|
import { useAuth } from './hooks/useAuth.js'
|
|
23
25
|
import { useTheme } from './hooks/useTheme.js'
|
|
24
26
|
import { buildWidgetRegistry, type WidgetDefinition } from './lib/define-widget.js'
|
|
@@ -64,7 +66,7 @@ export function ProtoApp({
|
|
|
64
66
|
}: ProtoAppProps) {
|
|
65
67
|
useTheme()
|
|
66
68
|
|
|
67
|
-
const { user, companyId, companies, profile, loading, signOut, setCompanyId } = useAuth()
|
|
69
|
+
const { user, role, companyId, companies, profile, loading, signOut, setCompanyId } = useAuth()
|
|
68
70
|
const [refreshKey, setRefreshKey] = useState(0)
|
|
69
71
|
const chatSendRef = useRef<((msg: string) => void) | null>(null)
|
|
70
72
|
|
|
@@ -140,24 +142,32 @@ export function ProtoApp({
|
|
|
140
142
|
const effectiveCompanyId = companyId || user.id
|
|
141
143
|
|
|
142
144
|
return (
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
145
|
+
<BrowserRouter>
|
|
146
|
+
<Routes>
|
|
147
|
+
<Route path="/admin" element={<AdminPanel widgets={widgetRegistry} />} />
|
|
148
|
+
<Route path="*" element={
|
|
149
|
+
<Shell
|
|
150
|
+
widgets={widgetRegistry}
|
|
151
|
+
defaultWidgets={defaultWidgets}
|
|
152
|
+
defaultLayouts={defaultLayouts}
|
|
153
|
+
cockpits={cockpits}
|
|
154
|
+
companyId={effectiveCompanyId}
|
|
155
|
+
refreshKey={refreshKey}
|
|
156
|
+
onSendToChat={onSendToChat}
|
|
157
|
+
activeEntity={activeEntity}
|
|
158
|
+
onActivateEntity={activateEntity}
|
|
159
|
+
onDeactivateEntity={() => setActiveEntity(null)}
|
|
160
|
+
openEntities={openEntities}
|
|
161
|
+
onCloseTab={closeEntityTab}
|
|
162
|
+
role={role}
|
|
163
|
+
companies={companies}
|
|
164
|
+
effectiveCompanyId={effectiveCompanyId}
|
|
165
|
+
setCompanyId={setCompanyId}
|
|
166
|
+
onSignOut={signOut}
|
|
167
|
+
userEmail={profile?.full_name || user.email || ''}
|
|
168
|
+
/>
|
|
169
|
+
} />
|
|
170
|
+
</Routes>
|
|
171
|
+
</BrowserRouter>
|
|
162
172
|
)
|
|
163
173
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useMemo, type ReactNode } from 'react'
|
|
2
|
-
import { useMountEffect } from '../hooks/useMountEffect'
|
|
2
|
+
import { useMountEffect } from '../hooks/useMountEffect.js'
|
|
3
3
|
import { ResponsiveGridLayout, type Layout } from 'react-grid-layout'
|
|
4
4
|
import 'react-grid-layout/css/styles.css'
|
|
5
5
|
import { XIcon } from 'lucide-react'
|
|
@@ -8,7 +8,7 @@ import { Toolbar } from './shell/Toolbar.js'
|
|
|
8
8
|
import { FocusView } from './shell/FocusView.js'
|
|
9
9
|
import { EmptyState } from './shell/EmptyState.js'
|
|
10
10
|
import type { ActiveEntity, WidgetInstance, WidgetType } from './shell/types.js'
|
|
11
|
-
import type { ShellContext, WidgetRegistry } from '../lib/define-widget'
|
|
11
|
+
import type { ShellContext, WidgetRegistry } from '../lib/define-widget.js'
|
|
12
12
|
|
|
13
13
|
export type { WidgetType, ActiveEntity } from './shell/types.js'
|
|
14
14
|
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react'
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
import { useAuth } from '../../hooks/useAuth.js'
|
|
4
|
+
import { useData } from '../../hooks/useData.js'
|
|
5
|
+
import { useTheme } from '../../hooks/useTheme.js'
|
|
6
|
+
import { supabase } from '../../lib/supabase.js'
|
|
7
|
+
import { Button } from '../ui/button.js'
|
|
8
|
+
import {
|
|
9
|
+
ArrowLeftIcon, UsersIcon, Building2Icon, WrenchIcon,
|
|
10
|
+
LayoutGridIcon, SearchIcon,
|
|
11
|
+
} from 'lucide-react'
|
|
12
|
+
import { SystemTab } from './SystemTab.js'
|
|
13
|
+
import type { WidgetRegistry } from '../../lib/define-widget.js'
|
|
14
|
+
|
|
15
|
+
type Tab = 'users' | 'companies' | 'widgets' | 'system'
|
|
16
|
+
|
|
17
|
+
interface AdminPanelProps {
|
|
18
|
+
widgets?: WidgetRegistry
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AdminPanel({ widgets }: AdminPanelProps) {
|
|
22
|
+
useTheme()
|
|
23
|
+
const navigate = useNavigate()
|
|
24
|
+
const { role, loading } = useAuth()
|
|
25
|
+
const [activeTab, setActiveTab] = useState<Tab>('users')
|
|
26
|
+
|
|
27
|
+
if (loading) {
|
|
28
|
+
return <div className="flex h-screen items-center justify-center text-muted-foreground">Loading...</div>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (role !== 'admin') {
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex h-screen items-center justify-center text-muted-foreground">
|
|
34
|
+
No tienes acceso a esta seccion.
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [
|
|
40
|
+
{ id: 'users', label: 'Usuarios', icon: <UsersIcon className="w-4 h-4" /> },
|
|
41
|
+
{ id: 'companies', label: 'Empresas', icon: <Building2Icon className="w-4 h-4" /> },
|
|
42
|
+
{ id: 'widgets', label: 'Widgets', icon: <LayoutGridIcon className="w-4 h-4" /> },
|
|
43
|
+
{ id: 'system', label: 'Sistema', icon: <WrenchIcon className="w-4 h-4" /> },
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="h-screen flex flex-col bg-background text-foreground">
|
|
48
|
+
<div className="border-b border-border px-4 py-3 flex items-center gap-3 bg-background/80 backdrop-blur">
|
|
49
|
+
<Button variant="ghost" size="sm" className="h-8 gap-1.5" onClick={() => navigate('/')}>
|
|
50
|
+
<ArrowLeftIcon className="w-4 h-4" /> Shell
|
|
51
|
+
</Button>
|
|
52
|
+
<div className="h-4 w-px bg-border" />
|
|
53
|
+
<h1 className="text-sm font-semibold">Admin</h1>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div className="flex flex-1 overflow-hidden">
|
|
57
|
+
<nav className="w-48 border-r border-border p-2 flex flex-col gap-0.5 shrink-0">
|
|
58
|
+
{tabs.map(tab => (
|
|
59
|
+
<button
|
|
60
|
+
key={tab.id}
|
|
61
|
+
onClick={() => setActiveTab(tab.id)}
|
|
62
|
+
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors ${
|
|
63
|
+
activeTab === tab.id
|
|
64
|
+
? 'bg-accent text-foreground font-medium'
|
|
65
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent/50'
|
|
66
|
+
}`}
|
|
67
|
+
>
|
|
68
|
+
{tab.icon}
|
|
69
|
+
{tab.label}
|
|
70
|
+
</button>
|
|
71
|
+
))}
|
|
72
|
+
</nav>
|
|
73
|
+
|
|
74
|
+
<main className="flex-1 overflow-y-auto p-6">
|
|
75
|
+
{activeTab === 'users' && <UsersTab />}
|
|
76
|
+
{activeTab === 'companies' && <CompaniesTab />}
|
|
77
|
+
{activeTab === 'widgets' && <WidgetsTab widgets={widgets} />}
|
|
78
|
+
{activeTab === 'system' && <SystemTab />}
|
|
79
|
+
</main>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ── Users Tab ─────────────────────────────────────────── */
|
|
86
|
+
|
|
87
|
+
function UsersTab() {
|
|
88
|
+
const [search, setSearch] = useState('')
|
|
89
|
+
|
|
90
|
+
const { data: users } = useData(async () => {
|
|
91
|
+
const { data: profiles } = await supabase
|
|
92
|
+
.from('profiles')
|
|
93
|
+
.select('id, full_name, role_title, onboarding_completed')
|
|
94
|
+
.order('full_name')
|
|
95
|
+
if (!profiles) return []
|
|
96
|
+
|
|
97
|
+
const { data: companies } = await supabase
|
|
98
|
+
.from('companies')
|
|
99
|
+
.select('id, name, owner_id')
|
|
100
|
+
|
|
101
|
+
const { data: memberships } = await supabase
|
|
102
|
+
.from('company_users')
|
|
103
|
+
.select('user_id, company_id')
|
|
104
|
+
|
|
105
|
+
return profiles.map((p: any) => {
|
|
106
|
+
const owned = (companies || []).filter((c: any) => c.owner_id === p.id)
|
|
107
|
+
const memberOf = (memberships || [])
|
|
108
|
+
.filter((m: any) => m.user_id === p.id)
|
|
109
|
+
.map((m: any) => (companies || []).find((c: any) => c.id === m.company_id))
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
return {
|
|
112
|
+
...p,
|
|
113
|
+
role: owned.length > 0 ? 'admin' : memberOf.length > 0 ? 'client' : 'none',
|
|
114
|
+
companies: owned.length > 0 ? owned : memberOf,
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}, [], [])
|
|
118
|
+
|
|
119
|
+
const filtered = useMemo(() => {
|
|
120
|
+
if (!search) return users
|
|
121
|
+
const q = search.toLowerCase()
|
|
122
|
+
return users.filter((u: any) =>
|
|
123
|
+
u.full_name?.toLowerCase().includes(q) ||
|
|
124
|
+
u.role_title?.toLowerCase().includes(q) ||
|
|
125
|
+
u.role?.includes(q)
|
|
126
|
+
)
|
|
127
|
+
}, [users, search])
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div>
|
|
131
|
+
<div className="flex items-center justify-between mb-4">
|
|
132
|
+
<h2 className="text-lg font-semibold">Usuarios</h2>
|
|
133
|
+
<div className="relative">
|
|
134
|
+
<SearchIcon className="w-4 h-4 absolute left-2.5 top-2.5 text-muted-foreground" />
|
|
135
|
+
<input
|
|
136
|
+
type="text"
|
|
137
|
+
placeholder="Buscar..."
|
|
138
|
+
value={search}
|
|
139
|
+
onChange={e => setSearch(e.target.value)}
|
|
140
|
+
className="pl-8 pr-3 py-2 text-sm bg-background border border-border rounded-md w-60 focus:outline-none focus:ring-1 focus:ring-primary"
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<div className="border border-border rounded-lg overflow-hidden">
|
|
145
|
+
<table className="w-full text-sm">
|
|
146
|
+
<thead>
|
|
147
|
+
<tr className="bg-muted/50 border-b border-border">
|
|
148
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Nombre</th>
|
|
149
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Cargo</th>
|
|
150
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Rol</th>
|
|
151
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Empresa</th>
|
|
152
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Onboarding</th>
|
|
153
|
+
</tr>
|
|
154
|
+
</thead>
|
|
155
|
+
<tbody>
|
|
156
|
+
{filtered.map((u: any) => (
|
|
157
|
+
<tr key={u.id} className="border-b border-border/50 hover:bg-accent/30 transition-colors">
|
|
158
|
+
<td className="px-4 py-2.5">{u.full_name || <span className="text-muted-foreground italic">Sin nombre</span>}</td>
|
|
159
|
+
<td className="px-4 py-2.5 text-muted-foreground">{u.role_title || '—'}</td>
|
|
160
|
+
<td className="px-4 py-2.5"><RoleBadge role={u.role} /></td>
|
|
161
|
+
<td className="px-4 py-2.5 text-muted-foreground">
|
|
162
|
+
{u.companies.map((c: any) => c.name).join(', ') || '—'}
|
|
163
|
+
</td>
|
|
164
|
+
<td className="px-4 py-2.5">
|
|
165
|
+
{u.onboarding_completed
|
|
166
|
+
? <span className="text-green-600 dark:text-green-400">Completo</span>
|
|
167
|
+
: <span className="text-amber-600 dark:text-amber-400">Pendiente</span>}
|
|
168
|
+
</td>
|
|
169
|
+
</tr>
|
|
170
|
+
))}
|
|
171
|
+
{filtered.length === 0 && (
|
|
172
|
+
<tr><td colSpan={5} className="px-4 py-8 text-center text-muted-foreground">No hay usuarios</td></tr>
|
|
173
|
+
)}
|
|
174
|
+
</tbody>
|
|
175
|
+
</table>
|
|
176
|
+
</div>
|
|
177
|
+
<p className="text-xs text-muted-foreground mt-2">{users.length} usuarios total</p>
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* ── Companies Tab ─────────────────────────────────────── */
|
|
183
|
+
|
|
184
|
+
function CompaniesTab() {
|
|
185
|
+
const { data: companies } = useData(async () => {
|
|
186
|
+
const { data } = await supabase
|
|
187
|
+
.from('companies')
|
|
188
|
+
.select('id, name, owner_id, created_at')
|
|
189
|
+
.order('name')
|
|
190
|
+
if (!data) return []
|
|
191
|
+
|
|
192
|
+
const { data: profiles } = await supabase.from('profiles').select('id, full_name')
|
|
193
|
+
const { data: memberships } = await supabase.from('company_users').select('company_id, user_id')
|
|
194
|
+
|
|
195
|
+
return data.map((c: any) => ({
|
|
196
|
+
...c,
|
|
197
|
+
owner_name: (profiles || []).find((p: any) => p.id === c.owner_id)?.full_name || '—',
|
|
198
|
+
member_count: (memberships || []).filter((m: any) => m.company_id === c.id).length,
|
|
199
|
+
}))
|
|
200
|
+
}, [], [])
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div>
|
|
204
|
+
<h2 className="text-lg font-semibold mb-4">Empresas</h2>
|
|
205
|
+
<div className="border border-border rounded-lg overflow-hidden">
|
|
206
|
+
<table className="w-full text-sm">
|
|
207
|
+
<thead>
|
|
208
|
+
<tr className="bg-muted/50 border-b border-border">
|
|
209
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Nombre</th>
|
|
210
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Owner</th>
|
|
211
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Miembros</th>
|
|
212
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Creada</th>
|
|
213
|
+
</tr>
|
|
214
|
+
</thead>
|
|
215
|
+
<tbody>
|
|
216
|
+
{companies.map((c: any) => (
|
|
217
|
+
<tr key={c.id} className="border-b border-border/50 hover:bg-accent/30 transition-colors">
|
|
218
|
+
<td className="px-4 py-2.5 font-medium">{c.name}</td>
|
|
219
|
+
<td className="px-4 py-2.5 text-muted-foreground">{c.owner_name}</td>
|
|
220
|
+
<td className="px-4 py-2.5 text-muted-foreground">{c.member_count}</td>
|
|
221
|
+
<td className="px-4 py-2.5 text-muted-foreground">
|
|
222
|
+
{new Date(c.created_at).toLocaleDateString()}
|
|
223
|
+
</td>
|
|
224
|
+
</tr>
|
|
225
|
+
))}
|
|
226
|
+
{companies.length === 0 && (
|
|
227
|
+
<tr><td colSpan={4} className="px-4 py-8 text-center text-muted-foreground">No hay empresas</td></tr>
|
|
228
|
+
)}
|
|
229
|
+
</tbody>
|
|
230
|
+
</table>
|
|
231
|
+
</div>
|
|
232
|
+
<p className="text-xs text-muted-foreground mt-2">{companies.length} empresas total</p>
|
|
233
|
+
</div>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* ── Widgets Tab ───────────────────────────────────────── */
|
|
238
|
+
|
|
239
|
+
function WidgetsTab({ widgets }: { widgets?: WidgetRegistry }) {
|
|
240
|
+
const entries = useMemo(() => {
|
|
241
|
+
if (!widgets) return []
|
|
242
|
+
return Array.from(widgets.values()).map(w => ({
|
|
243
|
+
type: w.type, title: w.title, icon: w.icon || '▦', category: w.category || 'general',
|
|
244
|
+
}))
|
|
245
|
+
}, [widgets])
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div>
|
|
249
|
+
<h2 className="text-lg font-semibold mb-4">Widgets registrados</h2>
|
|
250
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
251
|
+
{entries.map(w => (
|
|
252
|
+
<div key={w.type} className="border border-border rounded-lg p-4 hover:bg-accent/30 transition-colors">
|
|
253
|
+
<div className="flex items-center gap-2 mb-1">
|
|
254
|
+
<span className="text-lg">{w.icon}</span>
|
|
255
|
+
<span className="font-medium text-sm">{w.title}</span>
|
|
256
|
+
</div>
|
|
257
|
+
<div className="flex items-center gap-2 mt-2">
|
|
258
|
+
<span className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">{w.type}</span>
|
|
259
|
+
<span className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">{w.category}</span>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
))}
|
|
263
|
+
{entries.length === 0 && (
|
|
264
|
+
<p className="text-muted-foreground text-sm col-span-3">No hay widgets registrados</p>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
<p className="text-xs text-muted-foreground mt-3">{entries.length} widgets total</p>
|
|
268
|
+
</div>
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* ── Shared ────────────────────────────────────────────── */
|
|
273
|
+
|
|
274
|
+
function RoleBadge({ role }: { role: string }) {
|
|
275
|
+
const styles: Record<string, string> = {
|
|
276
|
+
admin: 'bg-primary/10 text-primary border-primary/20',
|
|
277
|
+
client: 'bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20',
|
|
278
|
+
none: 'bg-muted text-muted-foreground border-border',
|
|
279
|
+
}
|
|
280
|
+
return (
|
|
281
|
+
<span className={`text-xs px-2 py-0.5 rounded-full border ${styles[role] || styles.none}`}>
|
|
282
|
+
{role}
|
|
283
|
+
</span>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { useData } from '../../hooks/useData.js'
|
|
2
|
+
import { GATEWAY_URL, INTERNAL_SECRET } from '../../lib/config.js'
|
|
3
|
+
import {
|
|
4
|
+
WrenchIcon, BoxIcon, GitBranchIcon, BookOpenIcon,
|
|
5
|
+
} from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
interface McpMeta {
|
|
8
|
+
tools: { name: string; description: string }[]
|
|
9
|
+
entities: { name: string; table: string }[]
|
|
10
|
+
workflows: { name: string; entityTable: string; phases: string[] }[]
|
|
11
|
+
skills: { name: string; description: string | null; mcp_tools: string[] }[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function SystemTab() {
|
|
15
|
+
const { data: meta } = useData<McpMeta | null>(async () => {
|
|
16
|
+
try {
|
|
17
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
|
18
|
+
if (INTERNAL_SECRET) headers['X-Internal-Secret'] = INTERNAL_SECRET
|
|
19
|
+
const res = await fetch(`${GATEWAY_URL}/admin/meta`, { headers })
|
|
20
|
+
if (!res.ok) return null
|
|
21
|
+
return res.json()
|
|
22
|
+
} catch {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
}, [], null)
|
|
26
|
+
|
|
27
|
+
if (!meta) {
|
|
28
|
+
return (
|
|
29
|
+
<div>
|
|
30
|
+
<h2 className="text-lg font-semibold mb-4">Sistema</h2>
|
|
31
|
+
<p className="text-muted-foreground text-sm">
|
|
32
|
+
No se pudo conectar al gateway. Asegurate de que esta corriendo y que el endpoint <code>/admin/meta</code> esta disponible.
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="space-y-8">
|
|
40
|
+
{/* Tools */}
|
|
41
|
+
<section>
|
|
42
|
+
<div className="flex items-center gap-2 mb-3">
|
|
43
|
+
<WrenchIcon className="w-4 h-4 text-muted-foreground" />
|
|
44
|
+
<h2 className="text-lg font-semibold">Tools</h2>
|
|
45
|
+
<span className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">{meta.tools.length}</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div className="border border-border rounded-lg overflow-hidden">
|
|
48
|
+
<table className="w-full text-sm">
|
|
49
|
+
<thead>
|
|
50
|
+
<tr className="bg-muted/50 border-b border-border">
|
|
51
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Nombre</th>
|
|
52
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Descripcion</th>
|
|
53
|
+
</tr>
|
|
54
|
+
</thead>
|
|
55
|
+
<tbody>
|
|
56
|
+
{meta.tools.map(t => (
|
|
57
|
+
<tr key={t.name} className="border-b border-border/50 hover:bg-accent/30 transition-colors">
|
|
58
|
+
<td className="px-4 py-2 font-mono text-xs">{t.name}</td>
|
|
59
|
+
<td className="px-4 py-2 text-muted-foreground">{t.description}</td>
|
|
60
|
+
</tr>
|
|
61
|
+
))}
|
|
62
|
+
</tbody>
|
|
63
|
+
</table>
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
|
|
67
|
+
{/* Entities */}
|
|
68
|
+
<section>
|
|
69
|
+
<div className="flex items-center gap-2 mb-3">
|
|
70
|
+
<BoxIcon className="w-4 h-4 text-muted-foreground" />
|
|
71
|
+
<h2 className="text-lg font-semibold">Entities</h2>
|
|
72
|
+
<span className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">{meta.entities.length}</span>
|
|
73
|
+
</div>
|
|
74
|
+
{meta.entities.length > 0 ? (
|
|
75
|
+
<div className="border border-border rounded-lg overflow-hidden">
|
|
76
|
+
<table className="w-full text-sm">
|
|
77
|
+
<thead>
|
|
78
|
+
<tr className="bg-muted/50 border-b border-border">
|
|
79
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Nombre</th>
|
|
80
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Tabla</th>
|
|
81
|
+
</tr>
|
|
82
|
+
</thead>
|
|
83
|
+
<tbody>
|
|
84
|
+
{meta.entities.map((e: any) => (
|
|
85
|
+
<tr key={e.name} className="border-b border-border/50 hover:bg-accent/30 transition-colors">
|
|
86
|
+
<td className="px-4 py-2 font-medium">{e.name}</td>
|
|
87
|
+
<td className="px-4 py-2 font-mono text-xs text-muted-foreground">{e.table}</td>
|
|
88
|
+
</tr>
|
|
89
|
+
))}
|
|
90
|
+
</tbody>
|
|
91
|
+
</table>
|
|
92
|
+
</div>
|
|
93
|
+
) : (
|
|
94
|
+
<p className="text-sm text-muted-foreground">No hay entities definidas</p>
|
|
95
|
+
)}
|
|
96
|
+
</section>
|
|
97
|
+
|
|
98
|
+
{/* Workflows */}
|
|
99
|
+
<section>
|
|
100
|
+
<div className="flex items-center gap-2 mb-3">
|
|
101
|
+
<GitBranchIcon className="w-4 h-4 text-muted-foreground" />
|
|
102
|
+
<h2 className="text-lg font-semibold">Workflows</h2>
|
|
103
|
+
<span className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">{meta.workflows.length}</span>
|
|
104
|
+
</div>
|
|
105
|
+
{meta.workflows.length > 0 ? (
|
|
106
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
107
|
+
{meta.workflows.map((w: any) => (
|
|
108
|
+
<div key={w.name} className="border border-border rounded-lg p-4">
|
|
109
|
+
<div className="font-medium text-sm mb-1">{w.name}</div>
|
|
110
|
+
<div className="text-xs text-muted-foreground mb-2">Tabla: <code>{w.entityTable}</code></div>
|
|
111
|
+
<div className="flex flex-wrap gap-1">
|
|
112
|
+
{w.phases.map((p: string, i: number) => (
|
|
113
|
+
<span key={p} className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground flex items-center gap-1">
|
|
114
|
+
{i > 0 && <span className="text-muted-foreground/40">→</span>}
|
|
115
|
+
{p}
|
|
116
|
+
</span>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
) : (
|
|
123
|
+
<p className="text-sm text-muted-foreground">No hay workflows definidos</p>
|
|
124
|
+
)}
|
|
125
|
+
</section>
|
|
126
|
+
|
|
127
|
+
{/* Skills */}
|
|
128
|
+
<section>
|
|
129
|
+
<div className="flex items-center gap-2 mb-3">
|
|
130
|
+
<BookOpenIcon className="w-4 h-4 text-muted-foreground" />
|
|
131
|
+
<h2 className="text-lg font-semibold">Skills</h2>
|
|
132
|
+
<span className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">{meta.skills.length}</span>
|
|
133
|
+
</div>
|
|
134
|
+
{meta.skills.length > 0 ? (
|
|
135
|
+
<div className="border border-border rounded-lg overflow-hidden">
|
|
136
|
+
<table className="w-full text-sm">
|
|
137
|
+
<thead>
|
|
138
|
+
<tr className="bg-muted/50 border-b border-border">
|
|
139
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Nombre</th>
|
|
140
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Descripcion</th>
|
|
141
|
+
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Tools</th>
|
|
142
|
+
</tr>
|
|
143
|
+
</thead>
|
|
144
|
+
<tbody>
|
|
145
|
+
{meta.skills.map((s: any) => (
|
|
146
|
+
<tr key={s.name} className="border-b border-border/50 hover:bg-accent/30 transition-colors">
|
|
147
|
+
<td className="px-4 py-2 font-medium">{s.name}</td>
|
|
148
|
+
<td className="px-4 py-2 text-muted-foreground">{s.description || '—'}</td>
|
|
149
|
+
<td className="px-4 py-2">
|
|
150
|
+
<div className="flex flex-wrap gap-1">
|
|
151
|
+
{s.mcp_tools.map((t: string) => (
|
|
152
|
+
<span key={t} className="text-xs px-1.5 py-0.5 bg-muted rounded text-muted-foreground font-mono">{t}</span>
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
</td>
|
|
156
|
+
</tr>
|
|
157
|
+
))}
|
|
158
|
+
</tbody>
|
|
159
|
+
</table>
|
|
160
|
+
</div>
|
|
161
|
+
) : (
|
|
162
|
+
<p className="text-sm text-muted-foreground">No hay skills cargados</p>
|
|
163
|
+
)}
|
|
164
|
+
</section>
|
|
165
|
+
</div>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useState, useCallback, type ReactNode } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
import { useMountEffect } from '../../hooks/useMountEffect.js'
|
|
4
|
+
import { Button } from '../ui/button.js'
|
|
5
|
+
import { PlusIcon, RotateCcwIcon, SunIcon, MoonIcon, MonitorIcon, UserIcon, LogOutIcon, Building2Icon, ChevronDownIcon, CheckIcon, XIcon, HomeIcon, SettingsIcon, LayoutGridIcon, ShieldIcon } from 'lucide-react'
|
|
6
|
+
import { useTheme, type Theme } from '../../hooks/useTheme.js'
|
|
6
7
|
import type { ActiveEntity, WidgetType } from './types.js'
|
|
7
8
|
|
|
8
9
|
interface CatalogEntry {
|
|
@@ -55,6 +56,7 @@ export function Toolbar({
|
|
|
55
56
|
document.addEventListener('keydown', handler)
|
|
56
57
|
return () => document.removeEventListener('keydown', handler)
|
|
57
58
|
})
|
|
59
|
+
const navigate = useNavigate()
|
|
58
60
|
const currentCompany = companies?.find(c => c.id === effectiveCompanyId)
|
|
59
61
|
|
|
60
62
|
return (
|
|
@@ -197,6 +199,14 @@ export function Toolbar({
|
|
|
197
199
|
<SettingsIcon className="w-3.5 h-3.5" /> Configuracion
|
|
198
200
|
</button>
|
|
199
201
|
)}
|
|
202
|
+
{role === 'admin' && (
|
|
203
|
+
<button
|
|
204
|
+
onClick={() => { setOpenDropdown(null); navigate('/admin') }}
|
|
205
|
+
className="w-full text-left px-3 py-1.5 text-sm rounded hover:bg-accent transition-colors flex items-center gap-2"
|
|
206
|
+
>
|
|
207
|
+
<ShieldIcon className="w-3.5 h-3.5" /> Admin
|
|
208
|
+
</button>
|
|
209
|
+
)}
|
|
200
210
|
<ThemeToggleRow />
|
|
201
211
|
<div className="border-t border-border/50 mt-1 pt-1">
|
|
202
212
|
<button
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState, type ReactNode } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import { XIcon } from 'lucide-react'
|
|
4
|
-
import { cn } from '../../lib/utils'
|
|
4
|
+
import { cn } from '../../lib/utils.js'
|
|
5
5
|
|
|
6
6
|
interface ShellDialogProps {
|
|
7
7
|
open: boolean
|