This commit is contained in:
Iliyan Angelov
2025-11-20 02:18:52 +02:00
parent 34b4c969d4
commit 44e11520c5
55 changed files with 4741 additions and 876 deletions

View File

@@ -36,11 +36,12 @@ import systemSettingsService, {
CompanySettingsResponse,
UpdateCompanySettingsRequest,
} from '../../services/api/systemSettingsService';
import { recaptchaService, RecaptchaSettingsAdminResponse, UpdateRecaptchaSettingsRequest } from '../../services/api/systemSettingsService';
import { useCurrency } from '../../contexts/CurrencyContext';
import { Loading } from '../../components/common';
import { getCurrencySymbol } from '../../utils/format';
type SettingsTab = 'general' | 'cookie' | 'currency' | 'payment' | 'smtp' | 'company';
type SettingsTab = 'general' | 'cookie' | 'currency' | 'payment' | 'smtp' | 'company' | 'recaptcha';
const SettingsPage: React.FC = () => {
const { currency, supportedCurrencies, refreshCurrency } = useCurrency();
@@ -105,12 +106,22 @@ const SettingsPage: React.FC = () => {
company_phone: '',
company_email: '',
company_address: '',
tax_rate: 0,
});
const [logoPreview, setLogoPreview] = useState<string | null>(null);
const [faviconPreview, setFaviconPreview] = useState<string | null>(null);
const [uploadingLogo, setUploadingLogo] = useState(false);
const [uploadingFavicon, setUploadingFavicon] = useState(false);
// reCAPTCHA Settings State
const [recaptchaSettings, setRecaptchaSettings] = useState<RecaptchaSettingsAdminResponse['data'] | null>(null);
const [recaptchaFormData, setRecaptchaFormData] = useState<UpdateRecaptchaSettingsRequest>({
recaptcha_site_key: '',
recaptcha_secret_key: '',
recaptcha_enabled: false,
});
const [showRecaptchaSecret, setShowRecaptchaSecret] = useState(false);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
@@ -146,6 +157,9 @@ const SettingsPage: React.FC = () => {
if (activeTab === 'company') {
loadCompanySettings();
}
if (activeTab === 'recaptcha') {
loadRecaptchaSettings();
}
}, [activeTab]);
useEffect(() => {
@@ -219,6 +233,7 @@ const SettingsPage: React.FC = () => {
company_phone: companyRes.data.company_phone || '',
company_email: companyRes.data.company_email || '',
company_address: companyRes.data.company_address || '',
tax_rate: companyRes.data.tax_rate || 0,
});
// Set previews if URLs exist
@@ -579,6 +594,41 @@ const SettingsPage: React.FC = () => {
}
};
const loadRecaptchaSettings = async () => {
try {
const recaptchaRes = await recaptchaService.getRecaptchaSettingsAdmin();
setRecaptchaSettings(recaptchaRes.data);
setRecaptchaFormData({
recaptcha_site_key: recaptchaRes.data.recaptcha_site_key || '',
recaptcha_secret_key: '',
recaptcha_enabled: recaptchaRes.data.recaptcha_enabled || false,
});
} catch (error: any) {
toast.error(
error.response?.data?.detail ||
error.response?.data?.message ||
'Failed to load reCAPTCHA settings'
);
}
};
const handleSaveRecaptcha = async () => {
try {
setSaving(true);
await recaptchaService.updateRecaptchaSettings(recaptchaFormData);
toast.success('reCAPTCHA settings saved successfully');
await loadRecaptchaSettings();
} catch (error: any) {
toast.error(
error.response?.data?.detail ||
error.response?.data?.message ||
'Failed to save reCAPTCHA settings'
);
} finally {
setSaving(false);
}
};
if (loading) {
return <Loading fullScreen={false} text="Loading settings..." />;
}
@@ -590,6 +640,7 @@ const SettingsPage: React.FC = () => {
{ id: 'payment' as SettingsTab, label: 'Payment', icon: CreditCard },
{ id: 'smtp' as SettingsTab, label: 'Email Server', icon: Mail },
{ id: 'company' as SettingsTab, label: 'Company Info', icon: Building2 },
{ id: 'recaptcha' as SettingsTab, label: 'reCAPTCHA', icon: Shield },
];
return (
@@ -2154,6 +2205,29 @@ const SettingsPage: React.FC = () => {
Physical address of your company or hotel
</p>
</div>
{/* Tax Rate */}
<div className="space-y-4">
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
<DollarSign className="w-4 h-4 text-gray-600" />
Tax Rate (%)
</label>
<input
type="number"
step="0.01"
min="0"
max="100"
value={companyFormData.tax_rate || 0}
onChange={(e) =>
setCompanyFormData({ ...companyFormData, tax_rate: parseFloat(e.target.value) || 0 })
}
placeholder="0.00"
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
/>
<p className="text-xs text-gray-500">
Default tax rate percentage to be applied to all invoices (e.g., 10 for 10%). This will be used for all bookings unless overridden.
</p>
</div>
</div>
</div>
@@ -2178,6 +2252,152 @@ const SettingsPage: React.FC = () => {
</div>
</div>
)}
{activeTab === 'recaptcha' && (
<div className="space-y-8">
{/* Section Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
<Shield className="w-6 h-6 text-amber-600" />
Google reCAPTCHA Settings
</h2>
<p className="text-gray-600 mt-2">
Configure Google reCAPTCHA to protect your forms from spam and abuse
</p>
</div>
</div>
{/* reCAPTCHA Settings Form */}
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-8">
<div className="space-y-6">
{/* Enable/Disable Toggle */}
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg border border-gray-200">
<div>
<label className="text-sm font-semibold text-gray-900">
Enable reCAPTCHA
</label>
<p className="text-xs text-gray-500 mt-1">
Enable or disable reCAPTCHA verification across all forms
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={recaptchaFormData.recaptcha_enabled || false}
onChange={(e) =>
setRecaptchaFormData({
...recaptchaFormData,
recaptcha_enabled: e.target.checked,
})
}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-amber-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-amber-600"></div>
</label>
</div>
{/* Site Key */}
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
<Key className="w-4 h-4 text-gray-600" />
reCAPTCHA Site Key
</label>
<input
type="text"
value={recaptchaFormData.recaptcha_site_key || ''}
onChange={(e) =>
setRecaptchaFormData({
...recaptchaFormData,
recaptcha_site_key: e.target.value,
})
}
placeholder="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
/>
<p className="text-xs text-gray-500">
Your reCAPTCHA site key from Google. Get it from{' '}
<a
href="https://www.google.com/recaptcha/admin"
target="_blank"
rel="noopener noreferrer"
className="text-amber-600 hover:underline"
>
Google reCAPTCHA Admin
</a>
</p>
</div>
{/* Secret Key */}
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
<Lock className="w-4 h-4 text-gray-600" />
reCAPTCHA Secret Key
</label>
<div className="relative">
<input
type={showRecaptchaSecret ? 'text' : 'password'}
value={recaptchaFormData.recaptcha_secret_key || ''}
onChange={(e) =>
setRecaptchaFormData({
...recaptchaFormData,
recaptcha_secret_key: e.target.value,
})
}
placeholder={recaptchaSettings?.recaptcha_secret_key_masked || 'Enter secret key'}
className="w-full px-4 py-3.5 pr-12 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
/>
<button
type="button"
onClick={() => setShowRecaptchaSecret(!showRecaptchaSecret)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
>
{showRecaptchaSecret ? (
<EyeOff className="w-5 h-5" />
) : (
<Eye className="w-5 h-5" />
)}
</button>
</div>
<p className="text-xs text-gray-500">
Your reCAPTCHA secret key (keep this secure). Leave empty to keep existing value.
</p>
{recaptchaSettings?.recaptcha_secret_key_masked && (
<p className="text-xs text-amber-600">
Current: {recaptchaSettings.recaptcha_secret_key_masked}
</p>
)}
</div>
{/* Info Box */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-800">
<p className="font-semibold mb-1">About reCAPTCHA</p>
<p className="text-xs">
reCAPTCHA protects your forms from spam and abuse. You can use reCAPTCHA v2 (checkbox) or v3 (invisible).
Make sure to use the same version for both site key and secret key.
</p>
</div>
</div>
</div>
{/* Save Button */}
<div className="flex justify-end pt-4 border-t border-gray-200">
<button
onClick={handleSaveRecaptcha}
disabled={saving}
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
>
<Save className="w-5 h-5" />
{saving ? 'Saving...' : 'Save reCAPTCHA Settings'}
</button>
</div>
</div>
</div>
</div>
)}
</div>
</div>
);