diff --git a/drohnenfuehrer/frontend/src/components/admin/AdminPanel.js b/drohnenfuehrer/frontend/src/components/admin/AdminPanel.js index 39cec46..01ffc5e 100644 --- a/drohnenfuehrer/frontend/src/components/admin/AdminPanel.js +++ b/drohnenfuehrer/frontend/src/components/admin/AdminPanel.js @@ -3,7 +3,7 @@ import UserList from '../users/UserList'; import ExportButton from './ExportButton'; import Trash from './Trash'; import AuditLogs from './AuditLogs'; -import { updateAvailability, updateGPS, createUser, updateUser, deleteUser, uploadUserPhoto, deleteUserPhoto } from '../../services/users'; +import { updateAvailability, updateGPS, createUser, updateUser, deleteUser, uploadUserPhoto, deleteUserPhoto, generateInviteToken } from '../../services/users'; import { getFullConfig, updateConfig, uploadLogo, deleteLogo } from '../../services/config'; import ErrorMessage from '../common/ErrorMessage'; import './AdminPanel.css'; @@ -127,6 +127,16 @@ const AdminPanel = ({ users, loading, error, onRefetch }) => { } }; + const handleGenerateInvite = async (id) => { + const result = await generateInviteToken(id); + if (result.success) { + showNotification('success', 'Einladungs-Token erzeugt (7 Tage gueltig)'); + } else { + showNotification('error', result.message || 'Fehler beim Erzeugen des Einladungs-Tokens'); + } + return result; + }; + const handleConfigUpdate = async (updatedConfig) => { try { const response = await updateConfig(updatedConfig); @@ -187,7 +197,7 @@ const AdminPanel = ({ users, loading, error, onRefetch }) => { const file = e.target.files[0]; if (!file) return; if (!file.type.startsWith('image/')) { - showNotification('error', 'Nur Bilddateien erlaubt (JPG, PNG, SVG, ...)'); + showNotification('error', 'Nur Bilddateien erlaubt (JPG, PNG, WebP)'); return; } if (file.size > 500 * 1024) { @@ -291,6 +301,7 @@ const AdminPanel = ({ users, loading, error, onRefetch }) => { onUserDelete={handleUserDelete} onPhotoUpload={handleUserPhotoUpload} onPhotoDelete={handleUserPhotoDelete} + onGenerateInvite={handleGenerateInvite} /> > )} @@ -408,7 +419,7 @@ const AdminPanel = ({ users, loading, error, onRefetch }) => { )} -
Empfohlen: PNG, SVG oder JPG, max. 500 KB
+Erlaubt: PNG, JPG oder WebP, max. 500 KB (SVG wird aus Sicherheitsgruenden abgelehnt)
)} diff --git a/drohnenfuehrer/frontend/src/components/public/PublicUserList.js b/drohnenfuehrer/frontend/src/components/public/PublicUserList.js index 20cfeac..5a74055 100644 --- a/drohnenfuehrer/frontend/src/components/public/PublicUserList.js +++ b/drohnenfuehrer/frontend/src/components/public/PublicUserList.js @@ -267,7 +267,7 @@ const PublicUserList = ({ users, loading, onRefetch }) => {📍 {user.address}
- � {user.phone} + 📱 {user.phone} {user.landline && ( diff --git a/drohnenfuehrer/frontend/src/components/users/UserCard.css b/drohnenfuehrer/frontend/src/components/users/UserCard.css index 11e29ba..a6f6bb7 100644 --- a/drohnenfuehrer/frontend/src/components/users/UserCard.css +++ b/drohnenfuehrer/frontend/src/components/users/UserCard.css @@ -303,3 +303,30 @@ flex-direction: column; } } + +.invite-section { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--color-border); +} +.invite-section h4 { + margin: 0 0 0.5rem 0; + font-size: 0.9rem; + color: var(--color-text-muted); + font-weight: 600; +} +.invite-result { + margin-top: 0.5rem; +} +.invite-token { + display: block; + margin-top: 0.35rem; + padding: 0.4rem 0.5rem; + background: var(--color-bg-muted, #f2f2ec); + border: 1px solid var(--color-border); + border-radius: 2px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.72rem; + word-break: break-all; + user-select: all; +} diff --git a/drohnenfuehrer/frontend/src/components/users/UserCard.js b/drohnenfuehrer/frontend/src/components/users/UserCard.js index 9af125b..82ef0e7 100644 --- a/drohnenfuehrer/frontend/src/components/users/UserCard.js +++ b/drohnenfuehrer/frontend/src/components/users/UserCard.js @@ -2,12 +2,14 @@ import React, { useState, useRef } from 'react'; import { useConfigContext } from '../../contexts/ConfigContext'; import './UserCard.css'; -const UserCard = ({ user, onAvailabilityToggle, onGPSUpdate, onEdit, onDelete, onPhotoUpload, onPhotoDelete }) => { +const UserCard = ({ user, onAvailabilityToggle, onGPSUpdate, onEdit, onDelete, onPhotoUpload, onPhotoDelete, onGenerateInvite }) => { const { userTypeLabels } = useConfigContext(); const [lat, setLat] = useState(user.gps?.lat?.toString() || ''); const [lng, setLng] = useState(user.gps?.lng?.toString() || ''); const [isEditingGPS, setIsEditingGPS] = useState(false); const [photoUploading, setPhotoUploading] = useState(false); + const [invite, setInvite] = useState(null); + const [inviteLoading, setInviteLoading] = useState(false); const photoInputRef = useRef(null); const handleGPSUpdate = () => { @@ -50,6 +52,16 @@ const UserCard = ({ user, onAvailabilityToggle, onGPSUpdate, onEdit, onDelete, o } }; + const handleGenerateInvite = async () => { + if (!onGenerateInvite) return; + setInviteLoading(true); + const result = await onGenerateInvite(user._id); + if (result?.success) { + setInvite(result.data); + } + setInviteLoading(false); + }; + return (📍 {user.address}
@@ -120,6 +132,7 @@ const UserCard = ({ user, onAvailabilityToggle, onGPSUpdate, onEdit, onDelete, o
+ Keine E-Mail hinterlegt – ohne E-Mail ist kein Login möglich. +
+ ) : ( + <> + + {invite && ( ++ Diesen Token an {invite.email} weitergeben. Gültig bis{' '} + {new Date(invite.expiresAt).toLocaleDateString('de-DE')}. +
+{invite.inviteToken}
+ Empfohlen: PNG, SVG oder JPG, max. 500 KB
+Erlaubt: PNG, JPG oder WebP, max. 500 KB (SVG wird aus Sicherheitsgruenden abgelehnt)
📍 {user.address}
- � {user.phone} + 📱 {user.phone} {user.landline && ( diff --git a/nachsuche/frontend/src/components/users/UserCard.css b/nachsuche/frontend/src/components/users/UserCard.css index 11e29ba..a6f6bb7 100644 --- a/nachsuche/frontend/src/components/users/UserCard.css +++ b/nachsuche/frontend/src/components/users/UserCard.css @@ -303,3 +303,30 @@ flex-direction: column; } } + +.invite-section { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--color-border); +} +.invite-section h4 { + margin: 0 0 0.5rem 0; + font-size: 0.9rem; + color: var(--color-text-muted); + font-weight: 600; +} +.invite-result { + margin-top: 0.5rem; +} +.invite-token { + display: block; + margin-top: 0.35rem; + padding: 0.4rem 0.5rem; + background: var(--color-bg-muted, #f2f2ec); + border: 1px solid var(--color-border); + border-radius: 2px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.72rem; + word-break: break-all; + user-select: all; +} diff --git a/nachsuche/frontend/src/components/users/UserCard.js b/nachsuche/frontend/src/components/users/UserCard.js index 9af125b..6aa1f22 100644 --- a/nachsuche/frontend/src/components/users/UserCard.js +++ b/nachsuche/frontend/src/components/users/UserCard.js @@ -2,12 +2,14 @@ import React, { useState, useRef } from 'react'; import { useConfigContext } from '../../contexts/ConfigContext'; import './UserCard.css'; -const UserCard = ({ user, onAvailabilityToggle, onGPSUpdate, onEdit, onDelete, onPhotoUpload, onPhotoDelete }) => { +const UserCard = ({ user, onAvailabilityToggle, onGPSUpdate, onEdit, onDelete, onPhotoUpload, onPhotoDelete, onGenerateInvite }) => { const { userTypeLabels } = useConfigContext(); const [lat, setLat] = useState(user.gps?.lat?.toString() || ''); const [lng, setLng] = useState(user.gps?.lng?.toString() || ''); const [isEditingGPS, setIsEditingGPS] = useState(false); const [photoUploading, setPhotoUploading] = useState(false); + const [invite, setInvite] = useState(null); + const [inviteLoading, setInviteLoading] = useState(false); const photoInputRef = useRef(null); const handleGPSUpdate = () => { @@ -50,6 +52,16 @@ const UserCard = ({ user, onAvailabilityToggle, onGPSUpdate, onEdit, onDelete, o } }; + const handleGenerateInvite = async () => { + if (!onGenerateInvite) return; + setInviteLoading(true); + const result = await onGenerateInvite(user._id); + if (result?.success) { + setInvite(result.data); + } + setInviteLoading(false); + }; + return (📍 {user.address}
@@ -120,6 +132,7 @@ const UserCard = ({ user, onAvailabilityToggle, onGPSUpdate, onEdit, onDelete, o
+ Keine E-Mail hinterlegt – ohne E-Mail ist kein Login möglich. +
+ ) : ( + <> + + {invite && ( ++ Diesen Token an {invite.email} weitergeben. Gültig bis{' '} + {new Date(invite.expiresAt).toLocaleDateString('de-DE')}. +
+{invite.inviteToken}
+ Empfohlen: PNG, SVG oder JPG, max. 500 KB
+Erlaubt: PNG, JPG oder WebP, max. 500 KB (SVG wird aus Sicherheitsgruenden abgelehnt)
📍 {user.address}
- � {user.phone} + 📱 {user.phone} {user.landline && ( diff --git a/stoeberhunde/frontend/src/components/users/UserCard.css b/stoeberhunde/frontend/src/components/users/UserCard.css index 11e29ba..a6f6bb7 100644 --- a/stoeberhunde/frontend/src/components/users/UserCard.css +++ b/stoeberhunde/frontend/src/components/users/UserCard.css @@ -303,3 +303,30 @@ flex-direction: column; } } + +.invite-section { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--color-border); +} +.invite-section h4 { + margin: 0 0 0.5rem 0; + font-size: 0.9rem; + color: var(--color-text-muted); + font-weight: 600; +} +.invite-result { + margin-top: 0.5rem; +} +.invite-token { + display: block; + margin-top: 0.35rem; + padding: 0.4rem 0.5rem; + background: var(--color-bg-muted, #f2f2ec); + border: 1px solid var(--color-border); + border-radius: 2px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.72rem; + word-break: break-all; + user-select: all; +} diff --git a/stoeberhunde/frontend/src/components/users/UserCard.js b/stoeberhunde/frontend/src/components/users/UserCard.js index 9af125b..ca234f4 100644 --- a/stoeberhunde/frontend/src/components/users/UserCard.js +++ b/stoeberhunde/frontend/src/components/users/UserCard.js @@ -2,12 +2,14 @@ import React, { useState, useRef } from 'react'; import { useConfigContext } from '../../contexts/ConfigContext'; import './UserCard.css'; -const UserCard = ({ user, onAvailabilityToggle, onGPSUpdate, onEdit, onDelete, onPhotoUpload, onPhotoDelete }) => { +const UserCard = ({ user, onAvailabilityToggle, onGPSUpdate, onEdit, onDelete, onPhotoUpload, onPhotoDelete, onGenerateInvite }) => { const { userTypeLabels } = useConfigContext(); const [lat, setLat] = useState(user.gps?.lat?.toString() || ''); const [lng, setLng] = useState(user.gps?.lng?.toString() || ''); const [isEditingGPS, setIsEditingGPS] = useState(false); const [photoUploading, setPhotoUploading] = useState(false); + const [invite, setInvite] = useState(null); + const [inviteLoading, setInviteLoading] = useState(false); const photoInputRef = useRef(null); const handleGPSUpdate = () => { @@ -50,6 +52,16 @@ const UserCard = ({ user, onAvailabilityToggle, onGPSUpdate, onEdit, onDelete, o } }; + const handleGenerateInvite = async () => { + if (!onGenerateInvite) return; + setInviteLoading(true); + const result = await onGenerateInvite(user._id); + if (result?.success) { + setInvite(result.data); + } + setInviteLoading(false); + }; + return (📍 {user.address}
@@ -120,6 +132,7 @@ const UserCard = ({ user, onAvailabilityToggle, onGPSUpdate, onEdit, onDelete, o
+ Keine E-Mail hinterlegt – ohne E-Mail ist kein Login möglich. +
+ ) : ( + <> + + {invite && ( ++ Diesen Token an {invite.email} weitergeben. Gültig bis{' '} + {new Date(invite.expiresAt).toLocaleDateString('de-DE')}. +
+{invite.inviteToken}
+