Wire Azure OCR to central settings toggle; remove redundant Users section
- settings/src/integrations/schema.js: rename azure from 'Azure AD' to 'Azure Document Intelligence', swap fields to endpoint + api_key - Add use_global_azure column (migration + model) - global_settings_service: add azure to check_global_status and apply_global_overrides - api/settings.py: expose use_global_azure in response/update; apply overrides in test_azure_connection before credential check - Settings.tsx: add 'Use credentials from main stack settings' toggle for Azure OCR section (endpoint/key disabled when on, test button enabled when global is configured); remove Users section (managed centrally via auth service), clean up UserData interface, users query and mutations Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d7898ba897
commit
1564a66283
5 changed files with 44 additions and 161 deletions
|
|
@ -42,6 +42,7 @@ interface SettingsData {
|
|||
llm_monthly_token_limit: number
|
||||
llm_features_enabled: Record<string, boolean> | null
|
||||
use_global_smtp: boolean
|
||||
use_global_azure: boolean
|
||||
}
|
||||
|
||||
interface NewbookSettingsData {
|
||||
|
|
@ -105,16 +106,7 @@ interface RoomCategory {
|
|||
display_order: number
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
id: number
|
||||
email: string
|
||||
name: string | null
|
||||
is_active: boolean
|
||||
is_admin: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
type SettingsSection = 'account' | 'users' | 'access' | 'display' | 'azure' | 'email' | 'inbox' | 'dext' | 'newbook' | 'resos' | 'sambapos' | 'kds' | 'budget' | 'kitchen' | 'suppliers' | 'search' | 'nextcloud' | 'backup' | 'food_flags' | 'allergen_keywords' | 'ingredient_categories' | 'recipe_sections' | 'dish_courses' | 'api_access' | 'llm' | 'data'
|
||||
type SettingsSection = 'account' | 'access' | 'display' | 'azure' | 'email' | 'inbox' | 'dext' | 'newbook' | 'resos' | 'sambapos' | 'kds' | 'budget' | 'kitchen' | 'suppliers' | 'search' | 'nextcloud' | 'backup' | 'food_flags' | 'allergen_keywords' | 'ingredient_categories' | 'recipe_sections' | 'dish_courses' | 'api_access' | 'llm' | 'data'
|
||||
|
||||
interface SambaPOSSettingsData {
|
||||
sambapos_db_host: string | null
|
||||
|
|
@ -290,6 +282,7 @@ export default function Settings() {
|
|||
const [azureEndpoint, setAzureEndpoint] = useState('')
|
||||
const [azureKey, setAzureKey] = useState('')
|
||||
const [azureTestStatus, setAzureTestStatus] = useState<string | null>(null)
|
||||
const [useGlobalAzure, setUseGlobalAzure] = useState(false)
|
||||
// OCR post-processing options
|
||||
const [ocrCleanProductCodes, setOcrCleanProductCodes] = useState(false)
|
||||
const [ocrFilterSubtotalRows, setOcrFilterSubtotalRows] = useState(false)
|
||||
|
|
@ -611,21 +604,6 @@ export default function Settings() {
|
|||
},
|
||||
})
|
||||
|
||||
// Fetch users (admin only)
|
||||
const { data: users } = useQuery<UserData[]>({
|
||||
queryKey: ['users'],
|
||||
queryFn: async () => {
|
||||
const res = await fetch('/auth/users', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
if (!res.ok) {
|
||||
if (res.status === 403) return []
|
||||
throw new Error('Failed to fetch users')
|
||||
}
|
||||
return res.json()
|
||||
},
|
||||
enabled: !!user?.is_admin,
|
||||
})
|
||||
|
||||
// Fetch SambaPOS settings
|
||||
const { data: sambaSettings } = useQuery<SambaPOSSettingsData>({
|
||||
|
|
@ -905,6 +883,7 @@ export default function Settings() {
|
|||
useEffect(() => {
|
||||
if (settings) {
|
||||
setAzureEndpoint(settings.azure_endpoint || '')
|
||||
setUseGlobalAzure(settings.use_global_azure)
|
||||
setCurrencySymbol(settings.currency_symbol)
|
||||
setDateFormat(settings.date_format)
|
||||
setHighQuantityThreshold(settings.high_quantity_threshold)
|
||||
|
|
@ -1662,57 +1641,6 @@ export default function Settings() {
|
|||
},
|
||||
})
|
||||
|
||||
const toggleUserMutation = useMutation({
|
||||
mutationFn: async (userId: number) => {
|
||||
const res = await fetch(`/auth/users/${userId}/toggle-active`, {
|
||||
method: 'PATCH',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.detail || 'Failed to toggle user')
|
||||
}
|
||||
return res.json()
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||
},
|
||||
})
|
||||
|
||||
const deleteUserMutation = useMutation({
|
||||
mutationFn: async (userId: number) => {
|
||||
const res = await fetch(`/auth/users/${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.detail || 'Failed to delete user')
|
||||
}
|
||||
return res.json()
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||
},
|
||||
})
|
||||
|
||||
const toggleAdminMutation = useMutation({
|
||||
mutationFn: async (userId: number) => {
|
||||
const res = await fetch(`/auth/users/${userId}/toggle-admin`, {
|
||||
method: 'PATCH',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.detail || 'Failed to toggle admin status')
|
||||
}
|
||||
return res.json()
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||
},
|
||||
})
|
||||
|
||||
const savePageRestrictionsMutation = useMutation({
|
||||
mutationFn: async (pages: string[]) => {
|
||||
const res = await fetch('/kitchen/api/settings/page-restrictions', {
|
||||
|
|
@ -2543,6 +2471,7 @@ export default function Settings() {
|
|||
ocr_use_weight_as_quantity: ocrUseWeightAsQuantity,
|
||||
cost_distribution_max_days: costDistMaxDays,
|
||||
use_global_smtp: useGlobalSmtp,
|
||||
use_global_azure: useGlobalAzure,
|
||||
}
|
||||
if (azureKey) {
|
||||
data.azure_key = azureKey
|
||||
|
|
@ -2758,7 +2687,6 @@ export default function Settings() {
|
|||
|
||||
const sidebarItems: { id: SettingsSection; label: string; adminOnly?: boolean; href?: string; restrictPath?: string }[] = [
|
||||
{ id: 'account', label: 'Account' },
|
||||
{ id: 'users', label: 'Users', adminOnly: true, restrictPath: '/settings-users' },
|
||||
{ id: 'access', label: 'Access Control', adminOnly: true, restrictPath: '/settings-access' },
|
||||
{ id: 'display', label: 'Display', restrictPath: '/settings-display' },
|
||||
{ id: 'azure', label: 'Azure OCR', restrictPath: '/settings-azure' },
|
||||
|
|
@ -2933,82 +2861,6 @@ export default function Settings() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Users Section (Admin Only) */}
|
||||
{activeSection === 'users' && user?.is_admin && (
|
||||
<div style={styles.section}>
|
||||
<h2 style={styles.sectionTitle}>User Management</h2>
|
||||
<p style={styles.hint}>Manage users who have access to this kitchen.</p>
|
||||
|
||||
{/* Users List Block */}
|
||||
<div style={styles.settingsBlock}>
|
||||
<h3 style={styles.blockTitle}>Users</h3>
|
||||
{users && users.length > 0 && (
|
||||
<table style={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={styles.th}>Email</th>
|
||||
<th style={styles.th}>Name</th>
|
||||
<th style={styles.th}>Status</th>
|
||||
<th style={styles.th}>Role</th>
|
||||
<th style={styles.th}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((u) => (
|
||||
<tr key={u.id} style={!u.is_active ? styles.disabledRow : undefined}>
|
||||
<td style={styles.td}>{u.email}</td>
|
||||
<td style={styles.td}>{u.name || '-'}</td>
|
||||
<td style={styles.td}>
|
||||
<span style={u.is_active ? styles.activeStatus : styles.inactiveStatus}>
|
||||
{u.is_active ? 'Active' : 'Disabled'}
|
||||
</span>
|
||||
</td>
|
||||
<td style={styles.td}>{u.is_admin ? 'Admin' : 'User'}</td>
|
||||
<td style={styles.td}>
|
||||
{u.id !== user.id ? (
|
||||
<div style={styles.actionButtons}>
|
||||
<button
|
||||
onClick={() => toggleUserMutation.mutate(u.id)}
|
||||
style={u.is_active ? styles.disableBtn : styles.enableBtn}
|
||||
>
|
||||
{u.is_active ? 'Disable' : 'Enable'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm(`${u.is_admin ? 'Remove admin rights from' : 'Make admin'} ${u.email}?`)) {
|
||||
toggleAdminMutation.mutate(u.id)
|
||||
}
|
||||
}}
|
||||
style={u.is_admin ? styles.demoteBtn : styles.promoteBtn}
|
||||
>
|
||||
{u.is_admin ? 'Demote' : 'Make Admin'}
|
||||
</button>
|
||||
{!u.is_admin && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm(`Delete user ${u.email}?`)) {
|
||||
deleteUserMutation.mutate(u.id)
|
||||
}
|
||||
}}
|
||||
style={styles.deleteBtn}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span style={styles.youLabel}>(You)</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Access Control Section (Admin Only) */}
|
||||
{activeSection === 'access' && user?.is_admin && (
|
||||
<div style={styles.section}>
|
||||
|
|
@ -3110,7 +2962,6 @@ export default function Settings() {
|
|||
<div style={styles.checkboxGroup}>
|
||||
{[
|
||||
{ path: '/settings', label: 'Settings Page (entire page)' },
|
||||
{ path: '/settings-users', label: 'Users Management' },
|
||||
{ path: '/settings-access', label: 'Access Control' },
|
||||
{ path: '/settings-display', label: 'Display Settings' },
|
||||
{ path: '/settings-azure', label: 'Azure OCR' },
|
||||
|
|
@ -3262,6 +3113,21 @@ export default function Settings() {
|
|||
{/* API Configuration Block */}
|
||||
<div style={styles.settingsBlock}>
|
||||
<h3 style={styles.blockTitle}>API Configuration</h3>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '1rem', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useGlobalAzure}
|
||||
onChange={(e) => setUseGlobalAzure(e.target.checked)}
|
||||
/>
|
||||
<span style={{ fontWeight: 500 }}>Use credentials from main stack settings</span>
|
||||
{useGlobalAzure && (
|
||||
<span style={{ fontSize: '0.8rem', padding: '0.15rem 0.5rem', borderRadius: '10px',
|
||||
background: globalStatus?.azure ? '#d4edda' : '#fff3cd',
|
||||
color: globalStatus?.azure ? '#155724' : '#856404' }}>
|
||||
{globalStatus?.azure ? 'Configured' : 'Not configured in stack settings'}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<div style={styles.form}>
|
||||
<label style={styles.label}>
|
||||
Endpoint URL
|
||||
|
|
@ -3269,7 +3135,8 @@ export default function Settings() {
|
|||
type="text"
|
||||
value={azureEndpoint}
|
||||
onChange={(e) => setAzureEndpoint(e.target.value)}
|
||||
style={styles.input}
|
||||
disabled={useGlobalAzure}
|
||||
style={{ ...styles.input, ...(useGlobalAzure ? { opacity: 0.4, pointerEvents: 'none' as const } : {}) }}
|
||||
placeholder="https://your-resource.cognitiveservices.azure.com/"
|
||||
/>
|
||||
</label>
|
||||
|
|
@ -3279,15 +3146,13 @@ export default function Settings() {
|
|||
type="password"
|
||||
value={azureKey}
|
||||
onChange={(e) => setAzureKey(e.target.value)}
|
||||
style={styles.input}
|
||||
disabled={useGlobalAzure}
|
||||
style={{ ...styles.input, ...(useGlobalAzure ? { opacity: 0.4, pointerEvents: 'none' as const } : {}) }}
|
||||
placeholder={settings?.azure_key_set ? '••••••••••••••••' : 'Enter your API key'}
|
||||
/>
|
||||
{settings?.azure_key_set && !azureKey && <span style={styles.keyStatus}>Key is configured</span>}
|
||||
</label>
|
||||
</div>
|
||||
<p style={{ ...styles.hint, marginTop: '0.75rem' }}>
|
||||
Azure Document Intelligence uses app-specific credentials (not shared with the main stack).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* OCR Post-Processing Block */}
|
||||
|
|
@ -3328,7 +3193,7 @@ export default function Settings() {
|
|||
|
||||
{/* Actions - outside blocks */}
|
||||
<div style={styles.buttonRow}>
|
||||
<button onClick={() => azureTestMutation.mutate()} style={styles.testBtn} disabled={!settings?.azure_key_set}>
|
||||
<button onClick={() => azureTestMutation.mutate()} style={styles.testBtn} disabled={!settings?.azure_key_set && !(useGlobalAzure && globalStatus?.azure)}>
|
||||
Test Connection
|
||||
</button>
|
||||
<button onClick={handleSaveSettings} style={styles.saveBtn} disabled={updateMutation.isPending}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue