Admin: Kunden mit Portal-Passwort für project.webklar.com anlegen.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { databases, DATABASE_ID, COLLECTIONS, ID, Query, isDemoMode } from '../lib/appwrite'
|
import { databases, DATABASE_ID, COLLECTIONS, ID, Query, isDemoMode } from '../lib/appwrite'
|
||||||
|
import {
|
||||||
|
createCustomerWithPortalAccess,
|
||||||
|
updateCustomerWithPortalAccess,
|
||||||
|
} from '../lib/customerAdminApi'
|
||||||
|
|
||||||
const DEMO_MODE = isDemoMode
|
const DEMO_MODE = isDemoMode
|
||||||
|
|
||||||
@@ -50,20 +54,18 @@ export function useCustomers() {
|
|||||||
|
|
||||||
const createCustomer = async (data) => {
|
const createCustomer = async (data) => {
|
||||||
if (DEMO_MODE) {
|
if (DEMO_MODE) {
|
||||||
const newCustomer = { ...data, $id: Date.now().toString() }
|
const { password: _pw, ...rest } = data
|
||||||
|
const newCustomer = { ...rest, $id: Date.now().toString() }
|
||||||
setCustomers(prev => [...prev, newCustomer])
|
setCustomers(prev => [...prev, newCustomer])
|
||||||
return { success: true, data: newCustomer }
|
return { success: true, data: newCustomer }
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await databases.createDocument(
|
const { password, ...fields } = data
|
||||||
DATABASE_ID,
|
const result = await createCustomerWithPortalAccess({ ...fields, password })
|
||||||
COLLECTIONS.CUSTOMERS,
|
const customer = result.customer
|
||||||
ID.unique(),
|
setCustomers(prev => [...prev, customer])
|
||||||
data
|
return { success: true, data: customer }
|
||||||
)
|
|
||||||
setCustomers(prev => [...prev, response])
|
|
||||||
return { success: true, data: response }
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { success: false, error: err.message }
|
return { success: false, error: err.message }
|
||||||
}
|
}
|
||||||
@@ -71,19 +73,20 @@ export function useCustomers() {
|
|||||||
|
|
||||||
const updateCustomer = async (id, data) => {
|
const updateCustomer = async (id, data) => {
|
||||||
if (DEMO_MODE) {
|
if (DEMO_MODE) {
|
||||||
setCustomers(prev => prev.map(c => c.$id === id ? { ...c, ...data } : c))
|
const { password: _pw, ...rest } = data
|
||||||
|
setCustomers(prev => prev.map(c => c.$id === id ? { ...c, ...rest } : c))
|
||||||
return { success: true }
|
return { success: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await databases.updateDocument(
|
const { password, ...fields } = data
|
||||||
DATABASE_ID,
|
const payload = { ...fields }
|
||||||
COLLECTIONS.CUSTOMERS,
|
if (password) payload.password = password
|
||||||
id,
|
|
||||||
data
|
const result = await updateCustomerWithPortalAccess(id, payload)
|
||||||
)
|
const customer = result.customer
|
||||||
setCustomers(prev => prev.map(c => c.$id === id ? response : c))
|
setCustomers(prev => prev.map(c => c.$id === id ? customer : c))
|
||||||
return { success: true, data: response }
|
return { success: true, data: customer }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { success: false, error: err.message }
|
return { success: false, error: err.message }
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/lib/customerAdminApi.js
Normal file
32
src/lib/customerAdminApi.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { account } from './appwrite'
|
||||||
|
|
||||||
|
const PROJECT_ADMIN_URL =
|
||||||
|
import.meta.env.VITE_PROJECT_ADMIN_URL || 'https://project.webklar.com'
|
||||||
|
|
||||||
|
async function adminFetch(path, { method = 'POST', body } = {}) {
|
||||||
|
const jwt = await account.createJWT()
|
||||||
|
const response = await fetch(`${PROJECT_ADMIN_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt.jwt}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const data = await response.json().catch(() => ({}))
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || `API-Fehler ${response.status}`)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCustomerWithPortalAccess(payload) {
|
||||||
|
return adminFetch('/api/admin/customers', { method: 'POST', body: payload })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCustomerWithPortalAccess(customerId, payload) {
|
||||||
|
return adminFetch(`/api/admin/customers/${customerId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -27,7 +27,22 @@ export default function AdminPage() {
|
|||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [saveMessage, setSaveMessage] = useState('')
|
const [saveMessage, setSaveMessage] = useState('')
|
||||||
const [editingCustomer, setEditingCustomer] = useState(null)
|
const [editingCustomer, setEditingCustomer] = useState(null)
|
||||||
const [customerForm, setCustomerForm] = useState({ code: '', name: '', location: '', email: '', phone: '' })
|
const [customerForm, setCustomerForm] = useState({
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
location: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
password: '',
|
||||||
|
})
|
||||||
|
const emptyCustomerForm = {
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
location: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
password: '',
|
||||||
|
}
|
||||||
const [editingEmployee, setEditingEmployee] = useState(null)
|
const [editingEmployee, setEditingEmployee] = useState(null)
|
||||||
const [employeeForm, setEmployeeForm] = useState({ userId: '', displayName: '', email: '', shortcode: '' })
|
const [employeeForm, setEmployeeForm] = useState({ userId: '', displayName: '', email: '', shortcode: '' })
|
||||||
const [adminSection, setAdminSection] = useState('config')
|
const [adminSection, setAdminSection] = useState('config')
|
||||||
@@ -384,6 +399,7 @@ export default function AdminPage() {
|
|||||||
<th>Location</th>
|
<th>Location</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Phone</th>
|
<th>Phone</th>
|
||||||
|
<th>Portal</th>
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -433,14 +449,34 @@ export default function AdminPage() {
|
|||||||
onChange={(e) => setCustomerForm(prev => ({ ...prev, phone: e.target.value }))}
|
onChange={(e) => setCustomerForm(prev => ({ ...prev, phone: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control"
|
||||||
|
value={customerForm.password}
|
||||||
|
onChange={(e) => setCustomerForm(prev => ({ ...prev, password: e.target.value }))}
|
||||||
|
placeholder="Neu (optional)"
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
className="btn btn-green"
|
className="btn btn-green"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
if (!customerForm.email?.trim()) {
|
||||||
|
alert('E-Mail ist für den Portal-Login erforderlich.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (customerForm.password && customerForm.password.length < 8) {
|
||||||
|
alert('Passwort muss mindestens 8 Zeichen haben.')
|
||||||
|
return
|
||||||
|
}
|
||||||
const result = await updateCustomer(customer.$id, customerForm)
|
const result = await updateCustomer(customer.$id, customerForm)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setEditingCustomer(null)
|
setEditingCustomer(null)
|
||||||
setCustomerForm({ code: '', name: '', location: '', email: '', phone: '' })
|
setCustomerForm(emptyCustomerForm)
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Speichern: ' + (result.error || 'Unbekannter Fehler'))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -450,7 +486,7 @@ export default function AdminPage() {
|
|||||||
className="btn"
|
className="btn"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingCustomer(null)
|
setEditingCustomer(null)
|
||||||
setCustomerForm({ code: '', name: '', location: '', email: '', phone: '' })
|
setCustomerForm(emptyCustomerForm)
|
||||||
}}
|
}}
|
||||||
style={{ marginLeft: '4px' }}
|
style={{ marginLeft: '4px' }}
|
||||||
>
|
>
|
||||||
@@ -465,6 +501,13 @@ export default function AdminPage() {
|
|||||||
<td>{customer.location || '-'}</td>
|
<td>{customer.location || '-'}</td>
|
||||||
<td>{customer.email || '-'}</td>
|
<td>{customer.email || '-'}</td>
|
||||||
<td>{customer.phone || '-'}</td>
|
<td>{customer.phone || '-'}</td>
|
||||||
|
<td>
|
||||||
|
{customer.portalAccessEnabled ? (
|
||||||
|
<span style={{ color: '#059669', fontWeight: 600 }}>aktiv</span>
|
||||||
|
) : (
|
||||||
|
<span style={{ color: '#b45309' }}>inaktiv</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
className="btn"
|
className="btn"
|
||||||
@@ -475,7 +518,8 @@ export default function AdminPage() {
|
|||||||
name: customer.name || '',
|
name: customer.name || '',
|
||||||
location: customer.location || '',
|
location: customer.location || '',
|
||||||
email: customer.email || '',
|
email: customer.email || '',
|
||||||
phone: customer.phone || ''
|
phone: customer.phone || '',
|
||||||
|
password: '',
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -566,18 +610,47 @@ export default function AdminPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col col-9">
|
<div className="col col-3">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Portal-Passwort</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control"
|
||||||
|
value={customerForm.password}
|
||||||
|
onChange={(e) => setCustomerForm(prev => ({ ...prev, password: e.target.value }))}
|
||||||
|
placeholder="mind. 8 Zeichen"
|
||||||
|
autoComplete="new-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col col-6">
|
||||||
<div className="form-group" style={{ marginTop: '24px' }}>
|
<div className="form-group" style={{ marginTop: '24px' }}>
|
||||||
|
<p style={{ margin: '0 0 8px 0', fontSize: '13px', color: '#666' }}>
|
||||||
|
Der Kunde meldet sich damit unter{' '}
|
||||||
|
<a href="https://project.webklar.com/login.html" target="_blank" rel="noreferrer">
|
||||||
|
project.webklar.com
|
||||||
|
</a>{' '}
|
||||||
|
an (E-Mail + Passwort).
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
className="btn btn-green"
|
className="btn btn-green"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!customerForm.name) {
|
if (!customerForm.name?.trim()) {
|
||||||
alert('Bitte gib mindestens einen Namen ein.')
|
alert('Bitte gib mindestens einen Namen ein.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!customerForm.email?.trim()) {
|
||||||
|
alert('E-Mail ist für den Portal-Login erforderlich.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!customerForm.password || customerForm.password.length < 8) {
|
||||||
|
alert('Portal-Passwort muss mindestens 8 Zeichen haben.')
|
||||||
|
return
|
||||||
|
}
|
||||||
const result = await createCustomer(customerForm)
|
const result = await createCustomer(customerForm)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setCustomerForm({ code: '', name: '', location: '', email: '', phone: '' })
|
setCustomerForm(emptyCustomerForm)
|
||||||
} else {
|
} else {
|
||||||
alert('Fehler beim Erstellen: ' + (result.error || 'Unbekannter Fehler'))
|
alert('Fehler beim Erstellen: ' + (result.error || 'Unbekannter Fehler'))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user