diff --git a/management/src/App.jsx b/management/src/App.jsx
index 080be0a..d245cb9 100644
--- a/management/src/App.jsx
+++ b/management/src/App.jsx
@@ -7,6 +7,7 @@ import Layout from './components/Layout'
import Login from './pages/Login'
import Dashboard from './pages/Dashboard'
import Tenants from './pages/Tenants'
+import TenantUsersPage from './pages/TenantUsersPage'
import Users from './pages/Users'
import System from './pages/System'
@@ -25,6 +26,7 @@ function App() {
} />
} />
} />
+ } />
} />
} />
diff --git a/management/src/components/UserModal.jsx b/management/src/components/UserModal.jsx
new file mode 100644
index 0000000..96c7c80
--- /dev/null
+++ b/management/src/components/UserModal.jsx
@@ -0,0 +1,353 @@
+import React, { useState, useEffect } from 'react'
+import { XMarkIcon, EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
+
+const UserModal = ({ isOpen, onClose, onSave, user, tenant, title = "Create User" }) => {
+ const [formData, setFormData] = useState({
+ username: '',
+ email: '',
+ first_name: '',
+ last_name: '',
+ password: '',
+ confirmPassword: '',
+ role: 'viewer',
+ phone: '',
+ is_active: true
+ })
+ const [loading, setLoading] = useState(false)
+ const [errors, setErrors] = useState({})
+ const [showPassword, setShowPassword] = useState(false)
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false)
+
+ useEffect(() => {
+ if (user) {
+ // Editing existing user
+ setFormData({
+ username: user.username || '',
+ email: user.email || '',
+ first_name: user.first_name || '',
+ last_name: user.last_name || '',
+ password: '', // Never pre-fill passwords
+ confirmPassword: '',
+ role: user.role || 'viewer',
+ phone: user.phone || '',
+ is_active: user.is_active !== false
+ })
+ } else {
+ // Creating new user
+ setFormData({
+ username: '',
+ email: '',
+ first_name: '',
+ last_name: '',
+ password: '',
+ confirmPassword: '',
+ role: 'viewer',
+ phone: '',
+ is_active: true
+ })
+ }
+ setErrors({})
+ }, [user, isOpen])
+
+ const validateForm = () => {
+ const newErrors = {}
+
+ if (!formData.username.trim()) {
+ newErrors.username = 'Username is required'
+ }
+
+ if (!formData.email.trim()) {
+ newErrors.email = 'Email is required'
+ } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
+ newErrors.email = 'Email is invalid'
+ }
+
+ if (!user) { // Only require password for new users
+ if (!formData.password) {
+ newErrors.password = 'Password is required'
+ } else if (formData.password.length < 6) {
+ newErrors.password = 'Password must be at least 6 characters'
+ }
+
+ if (formData.password !== formData.confirmPassword) {
+ newErrors.confirmPassword = 'Passwords do not match'
+ }
+ } else if (formData.password) { // If editing and password provided
+ if (formData.password.length < 6) {
+ newErrors.password = 'Password must be at least 6 characters'
+ }
+ if (formData.password !== formData.confirmPassword) {
+ newErrors.confirmPassword = 'Passwords do not match'
+ }
+ }
+
+ setErrors(newErrors)
+ return Object.keys(newErrors).length === 0
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+
+ if (!validateForm()) return
+
+ setLoading(true)
+ try {
+ // Prepare data for submission
+ const submitData = { ...formData }
+ delete submitData.confirmPassword
+
+ // Don't send empty password for updates
+ if (user && !submitData.password) {
+ delete submitData.password
+ }
+
+ await onSave(submitData)
+ onClose()
+ } catch (error) {
+ console.error('Error saving user:', error)
+ setErrors({ general: error.message || 'Failed to save user' })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleInputChange = (e) => {
+ const { name, value, type, checked } = e.target
+ setFormData(prev => ({
+ ...prev,
+ [name]: type === 'checkbox' ? checked : value
+ }))
+
+ // Clear error when user starts typing
+ if (errors[name]) {
+ setErrors(prev => ({ ...prev, [name]: '' }))
+ }
+ }
+
+ if (!isOpen) return null
+
+ return (
+
@@ -200,6 +235,9 @@ const Tenants = () => {
Created
|
+
+ Status
+ |
Actions
|
@@ -236,8 +274,24 @@ const Tenants = () => {
{new Date(tenant.created_at).toLocaleDateString()}
|
+
+
+ |
+
|