- {members.map((m: any) => (
-
-
- {m.name?.[0] || '?'}
-
-
-
{m.name}
-
- {m.title || '-'} · {m.department_path || '-'}
- {m.email && ` · ${m.email}`}
+
+
+
setMemberSearch(e.target.value)} style={{ flex: 1, minWidth: '220px', fontSize: '13px' }} />
+
+ {selectedDepartment ? t('enterprise.org.selectedDepartment', { name: selectedDepartment.name }) : t('enterprise.org.allDepartments')}
+
+
+
+
+ {members.map((m) => (
+
+
+
{m.name}
+
+ {m.title || t('enterprise.org.noTitle')}
+
+
+ {m.department_path || '-'}
+
+
+
+ ))}
+ {members.length === 0 &&
{t('enterprise.org.noMembers')}
}
+
+
+
+ {t('enterprise.org.memberDetails')}
- ))}
- {members.length === 0 &&
{t('enterprise.org.noMembers')}
}
+ {selectedMember ? (
+ <>
+
+
+ {selectedMember.name?.[0] || '?'}
+
+
+
{selectedMember.name}
+
{selectedMember.title || t('enterprise.org.noTitle')}
+
+
+
+
+
{t('enterprise.org.departmentLabel')}
+
{selectedMember.department_path || '-'}
+
+
+
{t('enterprise.org.emailLabel')}
+
{selectedMember.email || '-'}
+
+
+
{t('enterprise.org.titleLabel')}
+
{selectedMember.title || t('enterprise.org.noTitle')}
+
+
+
{t('enterprise.org.provider')}
+
{selectedMember.provider === 'wecom' ? t('enterprise.org.providerWecom') : t('enterprise.org.providerFeishu')}
+
+
+ >
+ ) : (
+
+ {t('enterprise.org.selectMemberHint')}
+
+ )}
+
@@ -923,8 +1202,9 @@ function BroadcastSection() {
const { t } = useTranslation();
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
+ const [sendEmail, setSendEmail] = useState(false);
const [sending, setSending] = useState(false);
- const [result, setResult] = useState<{ users: number; agents: number } | null>(null);
+ const [result, setResult] = useState<{ users: number; agents: number; emails: number } | null>(null);
const handleSend = async () => {
if (!title.trim()) return;
@@ -935,7 +1215,7 @@ function BroadcastSection() {
const res = await fetch('/api/notifications/broadcast', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
- body: JSON.stringify({ title: title.trim(), body: body.trim() }),
+ body: JSON.stringify({ title: title.trim(), body: body.trim(), send_email: sendEmail }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
@@ -944,9 +1224,14 @@ function BroadcastSection() {
return;
}
const data = await res.json();
- setResult({ users: data.users_notified, agents: data.agents_notified });
+ setResult({
+ users: data.users_notified,
+ agents: data.agents_notified,
+ emails: data.emails_sent || 0,
+ });
setTitle('');
setBody('');
+ setSendEmail(false);
} catch (e: any) {
alert(e.message || 'Failed');
}
@@ -977,13 +1262,25 @@ function BroadcastSection() {
rows={3}
style={{ resize: 'vertical', fontSize: '13px', marginBottom: '12px' }}
/>
+
{result && (
- {t('enterprise.broadcast.sent', `Sent to ${result.users} users and ${result.agents} agents`, { users: result.users, agents: result.agents })}
+ {t(
+ 'enterprise.broadcast.sentWithEmail',
+ `Sent to ${result.users} users, ${result.agents} agents, and ${result.emails} email recipients`,
+ { users: result.users, agents: result.agents, emails: result.emails },
+ )}
)}
diff --git a/frontend/src/pages/ForgotPassword.tsx b/frontend/src/pages/ForgotPassword.tsx
new file mode 100644
index 00000000..26c05f92
--- /dev/null
+++ b/frontend/src/pages/ForgotPassword.tsx
@@ -0,0 +1,85 @@
+import { useEffect, useState } from 'react';
+import { Link } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { authApi } from '../services/api';
+
+export default function ForgotPassword() {
+ const { t } = useTranslation();
+ const [email, setEmail] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [message, setMessage] = useState('');
+
+ useEffect(() => {
+ document.documentElement.setAttribute('data-theme', 'dark');
+ }, []);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setMessage('');
+ setLoading(true);
+
+ try {
+ const res = await authApi.forgotPassword({ email: email.trim() });
+ setMessage(res.message);
+ } catch (err: any) {
+ setError(err.message || t('auth.forgotPasswordRequestFailed', 'Failed to request password reset'));
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+

+ Clawith
+
+
{t('auth.forgotPasswordTitle', 'Forgot password')}
+
+ {t('auth.forgotPasswordSubtitle', 'Enter your account email and we will send a reset link if the account exists.')}
+
+
+
+ {error && (
+
+ ⚠ {error}
+
+ )}
+
+ {message && (
+
+ ✓ {message}
+
+ )}
+
+
+
+
+ {t('auth.rememberedPassword', 'Remembered your password?')} {t('auth.backToLogin', 'Back to login')}
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx
index caffef70..85e70347 100644
--- a/frontend/src/pages/Login.tsx
+++ b/frontend/src/pages/Login.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../stores';
import { authApi } from '../services/api';
@@ -181,6 +181,17 @@ export default function Login() {
/>