2918 lines
155 KiB
TypeScript
2918 lines
155 KiB
TypeScript
import React, { useEffect, useState, useRef } from 'react';
|
|
import {
|
|
Settings,
|
|
Shield,
|
|
DollarSign,
|
|
CreditCard,
|
|
Save,
|
|
Info,
|
|
Globe,
|
|
SlidersHorizontal,
|
|
Eye,
|
|
EyeOff,
|
|
Lock,
|
|
Key,
|
|
Cookie,
|
|
Coins,
|
|
Sparkles,
|
|
Mail,
|
|
Building2,
|
|
Upload,
|
|
Image as ImageIcon,
|
|
MessageCircle,
|
|
Clock,
|
|
X,
|
|
CheckCircle2
|
|
} from 'lucide-react';
|
|
import { toast } from 'react-toastify';
|
|
import adminPrivacyService, {
|
|
CookieIntegrationSettings,
|
|
CookiePolicySettings,
|
|
CookiePolicySettingsResponse,
|
|
} from '../../services/api/adminPrivacyService';
|
|
import systemSettingsService, {
|
|
StripeSettingsResponse,
|
|
UpdateStripeSettingsRequest,
|
|
PayPalSettingsResponse,
|
|
UpdatePayPalSettingsRequest,
|
|
BoricaSettingsResponse,
|
|
UpdateBoricaSettingsRequest,
|
|
SmtpSettingsResponse,
|
|
UpdateSmtpSettingsRequest,
|
|
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' | 'recaptcha';
|
|
|
|
const SettingsPage: React.FC = () => {
|
|
const { currency, supportedCurrencies, refreshCurrency } = useCurrency();
|
|
const [activeTab, setActiveTab] = useState<SettingsTab>('general');
|
|
|
|
|
|
const [policy, setPolicy] = useState<CookiePolicySettings>({
|
|
analytics_enabled: true,
|
|
marketing_enabled: true,
|
|
preferences_enabled: true,
|
|
});
|
|
const [integrations, setIntegrations] = useState<CookieIntegrationSettings>({
|
|
ga_measurement_id: '',
|
|
fb_pixel_id: '',
|
|
});
|
|
const [policyMeta, setPolicyMeta] = useState<
|
|
Pick<CookiePolicySettingsResponse, 'updated_at' | 'updated_by'> | null
|
|
>(null);
|
|
|
|
|
|
const [selectedCurrency, setSelectedCurrency] = useState<string>(currency);
|
|
|
|
|
|
const [stripeSettings, setStripeSettings] = useState<StripeSettingsResponse['data'] | null>(null);
|
|
const [formData, setFormData] = useState<UpdateStripeSettingsRequest>({
|
|
stripe_secret_key: '',
|
|
stripe_publishable_key: '',
|
|
stripe_webhook_secret: '',
|
|
});
|
|
const [showSecretKey, setShowSecretKey] = useState(false);
|
|
const [showWebhookSecret, setShowWebhookSecret] = useState(false);
|
|
|
|
|
|
const [paypalSettings, setPaypalSettings] = useState<PayPalSettingsResponse['data'] | null>(null);
|
|
const [paypalFormData, setPaypalFormData] = useState<UpdatePayPalSettingsRequest>({
|
|
paypal_client_id: '',
|
|
paypal_client_secret: '',
|
|
paypal_mode: 'sandbox',
|
|
});
|
|
const [showPayPalSecret, setShowPayPalSecret] = useState(false);
|
|
|
|
|
|
const [boricaSettings, setBoricaSettings] = useState<BoricaSettingsResponse['data'] | null>(null);
|
|
const [boricaFormData, setBoricaFormData] = useState<UpdateBoricaSettingsRequest>({
|
|
borica_terminal_id: '',
|
|
borica_merchant_id: '',
|
|
borica_private_key_path: '',
|
|
borica_certificate_path: '',
|
|
borica_gateway_url: '',
|
|
borica_mode: 'test',
|
|
});
|
|
const [showBoricaTerminalId, setShowBoricaTerminalId] = useState(false);
|
|
const [showBoricaMerchantId, setShowBoricaMerchantId] = useState(false);
|
|
const [uploadingPrivateKey, setUploadingPrivateKey] = useState(false);
|
|
const [uploadingCertificate, setUploadingCertificate] = useState(false);
|
|
const privateKeyFileInputRef = useRef<HTMLInputElement>(null);
|
|
const certificateFileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
const [smtpSettings, setSmtpSettings] = useState<SmtpSettingsResponse['data'] | null>(null);
|
|
const [smtpFormData, setSmtpFormData] = useState<UpdateSmtpSettingsRequest>({
|
|
smtp_host: '',
|
|
smtp_port: '',
|
|
smtp_user: '',
|
|
smtp_password: '',
|
|
smtp_from_email: '',
|
|
smtp_from_name: '',
|
|
smtp_use_tls: true,
|
|
});
|
|
const [showSmtpPassword, setShowSmtpPassword] = useState(false);
|
|
const [testingEmail, setTestingEmail] = useState(false);
|
|
const [testEmailAddress, setTestEmailAddress] = useState('email@example.com');
|
|
|
|
|
|
const [companySettings, setCompanySettings] = useState<CompanySettingsResponse['data'] | null>(null);
|
|
const [companyFormData, setCompanyFormData] = useState<UpdateCompanySettingsRequest>({
|
|
company_name: '',
|
|
company_tagline: '',
|
|
company_phone: '',
|
|
company_email: '',
|
|
company_address: '',
|
|
tax_rate: 0,
|
|
chat_working_hours_start: 9,
|
|
chat_working_hours_end: 17,
|
|
});
|
|
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
|
const [faviconPreview, setFaviconPreview] = useState<string | null>(null);
|
|
const [uploadingLogo, setUploadingLogo] = useState(false);
|
|
const [uploadingFavicon, setUploadingFavicon] = useState(false);
|
|
|
|
|
|
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);
|
|
|
|
// Payment modal state
|
|
const [openPaymentModal, setOpenPaymentModal] = useState<'stripe' | 'paypal' | 'borica' | null>(null);
|
|
|
|
const currencyNames: Record<string, string> = {
|
|
VND: 'Vietnamese Dong',
|
|
USD: 'US Dollar',
|
|
EUR: 'Euro',
|
|
GBP: 'British Pound',
|
|
JPY: 'Japanese Yen',
|
|
CNY: 'Chinese Yuan',
|
|
KRW: 'South Korean Won',
|
|
SGD: 'Singapore Dollar',
|
|
THB: 'Thai Baht',
|
|
AUD: 'Australian Dollar',
|
|
CAD: 'Canadian Dollar',
|
|
};
|
|
|
|
const getCurrencyDisplayName = (code: string): string => {
|
|
const name = currencyNames[code] || code;
|
|
const symbol = getCurrencySymbol(code);
|
|
return `${name} (${symbol})`;
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
loadAllSettings();
|
|
}, []);
|
|
|
|
// Prevent body scrolling when payment modals are open
|
|
useEffect(() => {
|
|
if (openPaymentModal) {
|
|
document.body.style.overflow = 'hidden';
|
|
} else {
|
|
document.body.style.overflow = 'unset';
|
|
}
|
|
return () => {
|
|
document.body.style.overflow = 'unset';
|
|
};
|
|
}, [openPaymentModal]);
|
|
|
|
useEffect(() => {
|
|
if (activeTab === 'smtp') {
|
|
loadSmtpSettings();
|
|
}
|
|
if (activeTab === 'company') {
|
|
loadCompanySettings();
|
|
}
|
|
if (activeTab === 'recaptcha') {
|
|
loadRecaptchaSettings();
|
|
}
|
|
}, [activeTab]);
|
|
|
|
useEffect(() => {
|
|
setSelectedCurrency(currency);
|
|
}, [currency]);
|
|
|
|
const loadAllSettings = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [policyRes, integrationRes, currencyRes, stripeRes, paypalRes, boricaRes] = await Promise.all([
|
|
adminPrivacyService.getCookiePolicy(),
|
|
adminPrivacyService.getIntegrations(),
|
|
systemSettingsService.getPlatformCurrency(),
|
|
systemSettingsService.getStripeSettings(),
|
|
systemSettingsService.getPayPalSettings(),
|
|
systemSettingsService.getBoricaSettings(),
|
|
]);
|
|
|
|
setPolicy(policyRes.data);
|
|
setPolicyMeta({
|
|
updated_at: policyRes.updated_at,
|
|
updated_by: policyRes.updated_by,
|
|
});
|
|
setIntegrations(integrationRes.data || {});
|
|
|
|
setSelectedCurrency(currencyRes.data.currency);
|
|
|
|
setStripeSettings(stripeRes.data);
|
|
setFormData({
|
|
stripe_secret_key: '',
|
|
stripe_publishable_key: stripeRes.data.stripe_publishable_key || '',
|
|
stripe_webhook_secret: '',
|
|
});
|
|
setPaypalSettings(paypalRes.data);
|
|
setPaypalFormData({
|
|
paypal_client_id: '',
|
|
paypal_client_secret: '',
|
|
paypal_mode: paypalRes.data.paypal_mode || 'sandbox',
|
|
});
|
|
setBoricaSettings(boricaRes.data);
|
|
setBoricaFormData({
|
|
borica_terminal_id: '',
|
|
borica_merchant_id: '',
|
|
borica_private_key_path: '',
|
|
borica_certificate_path: '',
|
|
borica_gateway_url: '',
|
|
borica_mode: boricaRes.data.borica_mode || 'test',
|
|
});
|
|
} catch (error: any) {
|
|
toast.error(error.message || 'Failed to load settings');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const loadSmtpSettings = async () => {
|
|
try {
|
|
const smtpRes = await systemSettingsService.getSmtpSettings();
|
|
setSmtpSettings(smtpRes.data);
|
|
setSmtpFormData({
|
|
smtp_host: smtpRes.data.smtp_host || '',
|
|
smtp_port: smtpRes.data.smtp_port || '',
|
|
smtp_user: smtpRes.data.smtp_user || '',
|
|
smtp_password: '',
|
|
smtp_from_email: smtpRes.data.smtp_from_email || '',
|
|
smtp_from_name: smtpRes.data.smtp_from_name || '',
|
|
smtp_use_tls: smtpRes.data.smtp_use_tls,
|
|
});
|
|
} catch (error: any) {
|
|
toast.error(error.message || 'Failed to load SMTP settings');
|
|
}
|
|
};
|
|
|
|
const loadCompanySettings = async () => {
|
|
try {
|
|
const companyRes = await systemSettingsService.getCompanySettings();
|
|
setCompanySettings(companyRes.data);
|
|
setCompanyFormData({
|
|
company_name: companyRes.data.company_name || '',
|
|
company_tagline: companyRes.data.company_tagline || '',
|
|
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,
|
|
chat_working_hours_start: companyRes.data.chat_working_hours_start || 9,
|
|
chat_working_hours_end: companyRes.data.chat_working_hours_end || 17,
|
|
});
|
|
|
|
|
|
if (companyRes.data.company_logo_url) {
|
|
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
|
const logoUrl = companyRes.data.company_logo_url.startsWith('http')
|
|
? companyRes.data.company_logo_url
|
|
: `${baseUrl}${companyRes.data.company_logo_url}`;
|
|
setLogoPreview(logoUrl);
|
|
}
|
|
|
|
if (companyRes.data.company_favicon_url) {
|
|
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
|
const faviconUrl = companyRes.data.company_favicon_url.startsWith('http')
|
|
? companyRes.data.company_favicon_url
|
|
: `${baseUrl}${companyRes.data.company_favicon_url}`;
|
|
setFaviconPreview(faviconUrl);
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(error.message || 'Failed to load company settings');
|
|
}
|
|
};
|
|
|
|
|
|
const handleToggle = (key: keyof CookiePolicySettings) => {
|
|
setPolicy((prev) => ({
|
|
...prev,
|
|
[key]: !prev[key],
|
|
}));
|
|
};
|
|
|
|
const handleSaveCookie = async () => {
|
|
try {
|
|
setSaving(true);
|
|
const [policyRes, integrationRes] = await Promise.all([
|
|
adminPrivacyService.updateCookiePolicy(policy),
|
|
adminPrivacyService.updateIntegrations(integrations),
|
|
]);
|
|
setPolicy(policyRes.data);
|
|
setPolicyMeta({
|
|
updated_at: policyRes.updated_at,
|
|
updated_by: policyRes.updated_by,
|
|
});
|
|
setIntegrations(integrationRes.data || {});
|
|
toast.success('Cookie policy and integrations updated successfully');
|
|
} catch (error: any) {
|
|
toast.error(error.message || 'Failed to update cookie settings');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
|
|
const handleSaveCurrency = async () => {
|
|
try {
|
|
setSaving(true);
|
|
await systemSettingsService.updatePlatformCurrency(selectedCurrency);
|
|
await refreshCurrency();
|
|
await loadAllSettings();
|
|
toast.success('Platform currency updated successfully');
|
|
} catch (error: any) {
|
|
toast.error(error.message || 'Failed to update platform currency');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
|
|
const handleSaveStripe = async () => {
|
|
try {
|
|
setSaving(true);
|
|
const updateData: UpdateStripeSettingsRequest = {};
|
|
|
|
if (formData.stripe_secret_key && formData.stripe_secret_key.trim()) {
|
|
updateData.stripe_secret_key = formData.stripe_secret_key.trim();
|
|
}
|
|
|
|
if (formData.stripe_publishable_key && formData.stripe_publishable_key.trim()) {
|
|
updateData.stripe_publishable_key = formData.stripe_publishable_key.trim();
|
|
}
|
|
|
|
if (formData.stripe_webhook_secret && formData.stripe_webhook_secret.trim()) {
|
|
updateData.stripe_webhook_secret = formData.stripe_webhook_secret.trim();
|
|
}
|
|
|
|
await systemSettingsService.updateStripeSettings(updateData);
|
|
await loadAllSettings();
|
|
|
|
setFormData({
|
|
...formData,
|
|
stripe_secret_key: '',
|
|
stripe_webhook_secret: '',
|
|
});
|
|
|
|
toast.success('Stripe settings updated successfully');
|
|
setOpenPaymentModal(null);
|
|
} catch (error: any) {
|
|
toast.error(
|
|
error.response?.data?.message ||
|
|
error.response?.data?.detail ||
|
|
'Failed to update Stripe settings'
|
|
);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
|
|
const handleSavePayPal = async () => {
|
|
try {
|
|
setSaving(true);
|
|
const updateData: UpdatePayPalSettingsRequest = {};
|
|
|
|
if (paypalFormData.paypal_client_id && paypalFormData.paypal_client_id.trim()) {
|
|
updateData.paypal_client_id = paypalFormData.paypal_client_id.trim();
|
|
}
|
|
|
|
if (paypalFormData.paypal_client_secret && paypalFormData.paypal_client_secret.trim()) {
|
|
updateData.paypal_client_secret = paypalFormData.paypal_client_secret.trim();
|
|
}
|
|
|
|
if (paypalFormData.paypal_mode) {
|
|
updateData.paypal_mode = paypalFormData.paypal_mode;
|
|
}
|
|
|
|
await systemSettingsService.updatePayPalSettings(updateData);
|
|
await loadAllSettings();
|
|
|
|
setPaypalFormData({
|
|
...paypalFormData,
|
|
paypal_client_id: '',
|
|
paypal_client_secret: '',
|
|
});
|
|
|
|
toast.success('PayPal settings updated successfully');
|
|
setOpenPaymentModal(null);
|
|
} catch (error: any) {
|
|
toast.error(
|
|
error.response?.data?.message ||
|
|
error.response?.data?.detail ||
|
|
'Failed to update PayPal settings'
|
|
);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
|
|
const handleUploadBoricaFile = async (file: File, fileType: 'private_key' | 'certificate') => {
|
|
try {
|
|
if (fileType === 'private_key') {
|
|
setUploadingPrivateKey(true);
|
|
} else {
|
|
setUploadingCertificate(true);
|
|
}
|
|
|
|
// Validate file extension
|
|
const allowedExtensions = ['.pem', '.key', '.crt', '.cer', '.p12', '.pfx'];
|
|
const fileExt = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
|
|
if (!allowedExtensions.includes(fileExt)) {
|
|
throw new Error(`Invalid file type. Allowed extensions: ${allowedExtensions.join(', ')}`);
|
|
}
|
|
|
|
// Validate file size (1MB max)
|
|
const maxSize = 1024 * 1024; // 1MB
|
|
if (file.size > maxSize) {
|
|
throw new Error(`File size exceeds maximum allowed size of ${maxSize / 1024}KB`);
|
|
}
|
|
|
|
const response = await systemSettingsService.uploadBoricaCertificate(file, fileType);
|
|
|
|
// Reload settings to get updated paths
|
|
await loadAllSettings();
|
|
|
|
toast.success(response.message || `${fileType === 'private_key' ? 'Private key' : 'Certificate'} uploaded successfully`);
|
|
} catch (error: any) {
|
|
toast.error(
|
|
error.response?.data?.message ||
|
|
error.response?.data?.detail ||
|
|
error.message ||
|
|
`Failed to upload ${fileType === 'private_key' ? 'private key' : 'certificate'}`
|
|
);
|
|
} finally {
|
|
if (fileType === 'private_key') {
|
|
setUploadingPrivateKey(false);
|
|
} else {
|
|
setUploadingCertificate(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleSaveBorica = async () => {
|
|
try {
|
|
setSaving(true);
|
|
const updateData: UpdateBoricaSettingsRequest = {};
|
|
|
|
if (boricaFormData.borica_terminal_id && boricaFormData.borica_terminal_id.trim()) {
|
|
updateData.borica_terminal_id = boricaFormData.borica_terminal_id.trim();
|
|
}
|
|
|
|
if (boricaFormData.borica_merchant_id && boricaFormData.borica_merchant_id.trim()) {
|
|
updateData.borica_merchant_id = boricaFormData.borica_merchant_id.trim();
|
|
}
|
|
|
|
if (boricaFormData.borica_private_key_path && boricaFormData.borica_private_key_path.trim()) {
|
|
updateData.borica_private_key_path = boricaFormData.borica_private_key_path.trim();
|
|
}
|
|
|
|
if (boricaFormData.borica_certificate_path && boricaFormData.borica_certificate_path.trim()) {
|
|
updateData.borica_certificate_path = boricaFormData.borica_certificate_path.trim();
|
|
}
|
|
|
|
if (boricaFormData.borica_gateway_url && boricaFormData.borica_gateway_url.trim()) {
|
|
updateData.borica_gateway_url = boricaFormData.borica_gateway_url.trim();
|
|
}
|
|
|
|
if (boricaFormData.borica_mode) {
|
|
updateData.borica_mode = boricaFormData.borica_mode;
|
|
}
|
|
|
|
await systemSettingsService.updateBoricaSettings(updateData);
|
|
await loadAllSettings();
|
|
|
|
setBoricaFormData({
|
|
...boricaFormData,
|
|
borica_terminal_id: '',
|
|
borica_merchant_id: '',
|
|
borica_private_key_path: '',
|
|
borica_certificate_path: '',
|
|
borica_gateway_url: '',
|
|
});
|
|
|
|
toast.success('Borica settings updated successfully');
|
|
setOpenPaymentModal(null);
|
|
} catch (error: any) {
|
|
toast.error(
|
|
error.response?.data?.message ||
|
|
error.response?.data?.detail ||
|
|
'Failed to update Borica settings'
|
|
);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
|
|
const handleSaveSmtp = async () => {
|
|
try {
|
|
setSaving(true);
|
|
const updateData: UpdateSmtpSettingsRequest = {};
|
|
|
|
if (smtpFormData.smtp_host && smtpFormData.smtp_host.trim()) {
|
|
updateData.smtp_host = smtpFormData.smtp_host.trim();
|
|
}
|
|
|
|
if (smtpFormData.smtp_port && smtpFormData.smtp_port.trim()) {
|
|
updateData.smtp_port = smtpFormData.smtp_port.trim();
|
|
}
|
|
|
|
if (smtpFormData.smtp_user && smtpFormData.smtp_user.trim()) {
|
|
updateData.smtp_user = smtpFormData.smtp_user.trim();
|
|
}
|
|
|
|
if (smtpFormData.smtp_password && smtpFormData.smtp_password.trim()) {
|
|
updateData.smtp_password = smtpFormData.smtp_password.trim();
|
|
}
|
|
|
|
if (smtpFormData.smtp_from_email && smtpFormData.smtp_from_email.trim()) {
|
|
updateData.smtp_from_email = smtpFormData.smtp_from_email.trim();
|
|
}
|
|
|
|
if (smtpFormData.smtp_from_name && smtpFormData.smtp_from_name.trim()) {
|
|
updateData.smtp_from_name = smtpFormData.smtp_from_name.trim();
|
|
}
|
|
|
|
if (smtpFormData.smtp_use_tls !== undefined) {
|
|
updateData.smtp_use_tls = smtpFormData.smtp_use_tls;
|
|
}
|
|
|
|
await systemSettingsService.updateSmtpSettings(updateData);
|
|
await loadSmtpSettings();
|
|
|
|
setSmtpFormData({
|
|
...smtpFormData,
|
|
smtp_password: '',
|
|
});
|
|
|
|
toast.success('SMTP settings updated successfully');
|
|
} catch (error: any) {
|
|
toast.error(
|
|
error.response?.data?.message ||
|
|
error.response?.data?.detail ||
|
|
'Failed to update SMTP settings'
|
|
);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
|
|
const handleTestEmail = async () => {
|
|
if (!testEmailAddress || !testEmailAddress.trim()) {
|
|
toast.error('Please enter an email address');
|
|
return;
|
|
}
|
|
|
|
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(testEmailAddress.trim())) {
|
|
toast.error('Please enter a valid email address');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setTestingEmail(true);
|
|
const response = await systemSettingsService.testSmtpEmail(testEmailAddress.trim());
|
|
toast.success(`Test email sent successfully to ${response.data.recipient}`);
|
|
} catch (error: any) {
|
|
toast.error(
|
|
error.response?.data?.detail ||
|
|
error.response?.data?.message ||
|
|
'Failed to send test email. Please check your SMTP settings.'
|
|
);
|
|
} finally {
|
|
setTestingEmail(false);
|
|
}
|
|
};
|
|
|
|
|
|
const handleSaveCompany = async () => {
|
|
try {
|
|
setSaving(true);
|
|
await systemSettingsService.updateCompanySettings(companyFormData);
|
|
await loadCompanySettings();
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.dispatchEvent(new CustomEvent('refreshCompanySettings'));
|
|
}
|
|
toast.success('Company settings updated successfully');
|
|
} catch (error: any) {
|
|
toast.error(
|
|
error.response?.data?.message ||
|
|
error.response?.data?.detail ||
|
|
'Failed to update company settings'
|
|
);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
|
|
if (!file.type.startsWith('image/')) {
|
|
toast.error('Please select an image file');
|
|
return;
|
|
}
|
|
|
|
|
|
if (file.size > 2 * 1024 * 1024) {
|
|
toast.error('Logo size must be less than 2MB');
|
|
return;
|
|
}
|
|
|
|
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setLogoPreview(reader.result as string);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
|
|
try {
|
|
setUploadingLogo(true);
|
|
const response = await systemSettingsService.uploadCompanyLogo(file);
|
|
if (response.status === 'success') {
|
|
await loadCompanySettings();
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.dispatchEvent(new CustomEvent('refreshCompanySettings'));
|
|
}
|
|
toast.success('Logo uploaded successfully');
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(
|
|
error.response?.data?.detail ||
|
|
error.response?.data?.message ||
|
|
'Failed to upload logo'
|
|
);
|
|
setLogoPreview(null);
|
|
} finally {
|
|
setUploadingLogo(false);
|
|
|
|
e.target.value = '';
|
|
}
|
|
};
|
|
|
|
const handleFaviconUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
|
|
const validTypes = ['image/x-icon', 'image/vnd.microsoft.icon', 'image/png', 'image/svg+xml'];
|
|
const validExtensions = ['.ico', '.png', '.svg'];
|
|
const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
|
|
|
|
if (!validTypes.includes(file.type) && !validExtensions.includes(fileExtension)) {
|
|
toast.error('Favicon must be .ico, .png, or .svg file');
|
|
return;
|
|
}
|
|
|
|
|
|
if (file.size > 500 * 1024) {
|
|
toast.error('Favicon size must be less than 500KB');
|
|
return;
|
|
}
|
|
|
|
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setFaviconPreview(reader.result as string);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
|
|
try {
|
|
setUploadingFavicon(true);
|
|
const response = await systemSettingsService.uploadCompanyFavicon(file);
|
|
if (response.status === 'success') {
|
|
await loadCompanySettings();
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.dispatchEvent(new CustomEvent('refreshCompanySettings'));
|
|
}
|
|
toast.success('Favicon uploaded successfully');
|
|
|
|
const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
|
|
if (link) {
|
|
link.href = response.data.full_url;
|
|
} else {
|
|
const newLink = document.createElement('link');
|
|
newLink.rel = 'icon';
|
|
newLink.href = response.data.full_url;
|
|
document.head.appendChild(newLink);
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(
|
|
error.response?.data?.detail ||
|
|
error.response?.data?.message ||
|
|
'Failed to upload favicon'
|
|
);
|
|
setFaviconPreview(null);
|
|
} finally {
|
|
setUploadingFavicon(false);
|
|
|
|
e.target.value = '';
|
|
}
|
|
};
|
|
|
|
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..." />;
|
|
}
|
|
|
|
const tabs = [
|
|
{ id: 'general' as SettingsTab, label: 'Overview', icon: Settings },
|
|
{ id: 'cookie' as SettingsTab, label: 'Privacy & Cookies', icon: Cookie },
|
|
{ id: 'currency' as SettingsTab, label: 'Currency', icon: Coins },
|
|
{ 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 (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50">
|
|
<div className="max-w-7xl mx-auto px-2 sm:px-3 md:px-4 lg:px-6 xl:px-8 py-4 sm:py-6 md:py-8 space-y-6 sm:space-y-8 md:space-y-10 animate-fade-in">
|
|
{}
|
|
<div className="relative">
|
|
{}
|
|
<div className="absolute inset-0 bg-gradient-to-r from-amber-400/5 via-transparent to-amber-600/5 rounded-3xl blur-3xl"></div>
|
|
<div className="relative bg-white/80 backdrop-blur-xl rounded-2xl sm:rounded-3xl shadow-2xl border border-amber-200/30 p-4 sm:p-6 md:p-8 lg:p-10">
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 sm:gap-6 md:gap-8">
|
|
<div className="flex items-start gap-3 sm:gap-4 md:gap-5">
|
|
<div className="relative flex-shrink-0">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-amber-400 to-amber-600 rounded-xl sm:rounded-2xl blur-lg opacity-50"></div>
|
|
<div className="relative p-2.5 sm:p-3 md:p-4 rounded-xl sm:rounded-2xl bg-gradient-to-br from-amber-500 via-amber-500 to-amber-600 shadow-xl border border-amber-400/50">
|
|
<Settings className="w-5 h-5 sm:w-6 sm:h-6 md:w-8 md:h-8 text-white" />
|
|
<div className="absolute -top-1 -right-1 w-3 h-3 sm:w-4 sm:h-4 bg-gradient-to-br from-yellow-300 to-amber-500 rounded-full shadow-lg animate-pulse"></div>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2 sm:space-y-3 flex-1">
|
|
<div className="flex items-center gap-2 sm:gap-3 flex-wrap">
|
|
<h1 className="text-2xl sm:text-3xl md:text-3xl font-extrabold bg-gradient-to-r from-slate-900 via-amber-700 to-slate-900 bg-clip-text text-transparent">
|
|
Settings Dashboard
|
|
</h1>
|
|
<Sparkles className="w-4 h-4 sm:w-5 sm:h-5 text-amber-500 animate-pulse" />
|
|
</div>
|
|
<p className="text-gray-600 text-xs sm:text-sm md:text-sm max-w-2xl leading-relaxed">
|
|
Centralized control center for all platform configurations and system settings
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="mt-4 sm:mt-6 md:mt-8 lg:mt-10 pt-4 sm:pt-6 md:pt-8 border-t border-gradient-to-r from-transparent via-amber-200/30 to-transparent">
|
|
<div className="overflow-x-auto -mx-2 sm:-mx-3 px-2 sm:px-3 scrollbar-hide">
|
|
<div className="flex gap-2 sm:gap-3 min-w-max sm:min-w-0 sm:flex-wrap">
|
|
{tabs.map((tab) => {
|
|
const Icon = tab.icon;
|
|
const isActive = activeTab === tab.id;
|
|
return (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`
|
|
group relative flex items-center gap-1.5 sm:gap-2 md:gap-3 px-3 sm:px-4 md:px-6 py-2 sm:py-2.5 md:py-3.5 rounded-lg sm:rounded-xl font-semibold text-xs sm:text-sm flex-shrink-0
|
|
transition-all duration-300 overflow-hidden
|
|
${
|
|
isActive
|
|
? 'bg-gradient-to-r from-amber-500 via-amber-500 to-amber-600 text-white shadow-xl shadow-amber-500/40 scale-105'
|
|
: 'bg-white/80 text-gray-700 border border-gray-200/60 hover:border-amber-300/60 hover:bg-gradient-to-r hover:from-amber-50/50 hover:to-amber-50/30 hover:shadow-lg hover:scale-102'
|
|
}
|
|
`}
|
|
>
|
|
{isActive && (
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-shimmer"></div>
|
|
)}
|
|
<Icon className={`w-3.5 h-3.5 sm:w-4 sm:h-4 md:w-5 md:h-5 transition-transform duration-300 flex-shrink-0 ${isActive ? 'text-white' : 'text-gray-600 group-hover:text-amber-600 group-hover:scale-110'}`} />
|
|
<span className="relative z-10 whitespace-nowrap">{tab.label}</span>
|
|
{isActive && (
|
|
<div className="absolute bottom-0 left-0 right-0 h-0.5 sm:h-1 bg-gradient-to-r from-yellow-300 via-amber-400 to-yellow-300"></div>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
{activeTab === 'general' && (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5 md:gap-6 lg:gap-8">
|
|
<div
|
|
onClick={() => setActiveTab('cookie')}
|
|
className="group relative bg-white/90 backdrop-blur-xl rounded-xl sm:rounded-2xl shadow-xl border border-blue-100/50 p-4 sm:p-6 md:p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-[1.02] sm:hover:scale-105 hover:border-blue-300/60 overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 w-24 h-24 sm:w-32 sm:h-32 bg-gradient-to-br from-blue-400/10 to-transparent rounded-bl-full"></div>
|
|
<div className="relative space-y-3 sm:space-y-4 md:space-y-5">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-2 sm:gap-3 md:gap-4">
|
|
<div className="relative flex-shrink-0">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-400 to-blue-600 rounded-lg sm:rounded-xl blur-md opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
|
<div className="relative p-2 sm:p-2.5 md:p-3.5 rounded-lg sm:rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 shadow-lg border border-blue-400/50 group-hover:scale-110 transition-transform">
|
|
<Shield className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-gray-900 text-sm sm:text-base md:text-lg mb-1">Privacy & Cookies</h3>
|
|
<div className="h-0.5 sm:h-1 w-8 sm:w-12 bg-gradient-to-r from-blue-500 to-blue-600 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-gray-600 text-xs sm:text-sm leading-relaxed">
|
|
Manage cookie preferences, analytics integrations, and privacy controls
|
|
</p>
|
|
<div className="pt-3 sm:pt-4 md:pt-5 border-t border-gray-100">
|
|
<div className="flex items-center justify-between text-xs sm:text-sm">
|
|
<span className="text-gray-500 font-medium">Last updated</span>
|
|
<span className="font-semibold text-gray-800 bg-gray-50 px-2 sm:px-3 py-1 rounded-lg text-xs sm:text-sm">
|
|
{policyMeta?.updated_at
|
|
? new Date(policyMeta.updated_at).toLocaleDateString()
|
|
: 'Never'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-blue-50/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
|
</div>
|
|
|
|
<div
|
|
onClick={() => setActiveTab('currency')}
|
|
className="group relative bg-white/90 backdrop-blur-xl rounded-xl sm:rounded-2xl shadow-xl border border-emerald-100/50 p-4 sm:p-6 md:p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-[1.02] sm:hover:scale-105 hover:border-emerald-300/60 overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 w-24 h-24 sm:w-32 sm:h-32 bg-gradient-to-br from-emerald-400/10 to-transparent rounded-bl-full"></div>
|
|
<div className="relative space-y-3 sm:space-y-4 md:space-y-5">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-2 sm:gap-3 md:gap-4">
|
|
<div className="relative flex-shrink-0">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-emerald-400 to-emerald-600 rounded-lg sm:rounded-xl blur-md opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
|
<div className="relative p-2 sm:p-2.5 md:p-3.5 rounded-lg sm:rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 shadow-lg border border-emerald-400/50 group-hover:scale-110 transition-transform">
|
|
<DollarSign className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-gray-900 text-sm sm:text-base md:text-lg mb-1">Currency</h3>
|
|
<div className="h-0.5 sm:h-1 w-8 sm:w-12 bg-gradient-to-r from-emerald-500 to-emerald-600 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-gray-600 text-xs sm:text-sm leading-relaxed">
|
|
Configure platform-wide currency settings and display preferences
|
|
</p>
|
|
<div className="pt-3 sm:pt-4 md:pt-5 border-t border-gray-100">
|
|
<div className="flex items-center justify-between text-xs sm:text-sm">
|
|
<span className="text-gray-500 font-medium">Current currency</span>
|
|
<span className="font-bold text-emerald-600 bg-emerald-50 px-2 sm:px-3 py-1 rounded-lg text-xs sm:text-sm md:text-base">
|
|
{currency}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-emerald-50/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
|
</div>
|
|
|
|
<div
|
|
onClick={() => setActiveTab('payment')}
|
|
className="group relative bg-white/90 backdrop-blur-xl rounded-xl sm:rounded-2xl shadow-xl border border-indigo-100/50 p-4 sm:p-6 md:p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-[1.02] sm:hover:scale-105 hover:border-indigo-300/60 overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 w-24 h-24 sm:w-32 sm:h-32 bg-gradient-to-br from-indigo-400/10 to-transparent rounded-bl-full"></div>
|
|
<div className="relative space-y-3 sm:space-y-4 md:space-y-5">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-2 sm:gap-3 md:gap-4">
|
|
<div className="relative flex-shrink-0">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-indigo-400 to-indigo-600 rounded-lg sm:rounded-xl blur-md opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
|
<div className="relative p-2 sm:p-2.5 md:p-3.5 rounded-lg sm:rounded-xl bg-gradient-to-br from-indigo-500 to-indigo-600 shadow-lg border border-indigo-400/50 group-hover:scale-110 transition-transform">
|
|
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-gray-900 text-sm sm:text-base md:text-lg mb-1">Payment Gateway</h3>
|
|
<div className="h-0.5 sm:h-1 w-8 sm:w-12 bg-gradient-to-r from-indigo-500 to-indigo-600 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-gray-600 text-xs sm:text-sm leading-relaxed">
|
|
Manage Stripe payment processing credentials and webhook settings
|
|
</p>
|
|
<div className="pt-3 sm:pt-4 md:pt-5 border-t border-gray-100">
|
|
<div className="flex items-center justify-between text-xs sm:text-sm">
|
|
<span className="text-gray-500 font-medium">Status</span>
|
|
<span className={`font-bold px-2 sm:px-3 py-1 rounded-lg text-xs sm:text-sm ${
|
|
stripeSettings?.has_secret_key && stripeSettings?.has_publishable_key
|
|
? 'text-emerald-600 bg-emerald-50'
|
|
: 'text-gray-400 bg-gray-50'
|
|
}`}>
|
|
{stripeSettings?.has_secret_key && stripeSettings?.has_publishable_key ? '✓ Configured' : 'Not configured'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-indigo-50/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
|
</div>
|
|
|
|
<div
|
|
onClick={() => setActiveTab('smtp')}
|
|
className="group relative bg-white/90 backdrop-blur-xl rounded-xl sm:rounded-2xl shadow-xl border border-teal-100/50 p-4 sm:p-6 md:p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-[1.02] sm:hover:scale-105 hover:border-teal-300/60 overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 w-24 h-24 sm:w-32 sm:h-32 bg-gradient-to-br from-teal-400/10 to-transparent rounded-bl-full"></div>
|
|
<div className="relative space-y-3 sm:space-y-4 md:space-y-5">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-2 sm:gap-3 md:gap-4">
|
|
<div className="relative flex-shrink-0">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-teal-400 to-teal-600 rounded-lg sm:rounded-xl blur-md opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
|
<div className="relative p-2 sm:p-2.5 md:p-3.5 rounded-lg sm:rounded-xl bg-gradient-to-br from-teal-500 to-teal-600 shadow-lg border border-teal-400/50 group-hover:scale-110 transition-transform">
|
|
<Mail className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-gray-900 text-sm sm:text-base md:text-lg mb-1">Email Server</h3>
|
|
<div className="h-0.5 sm:h-1 w-8 sm:w-12 bg-gradient-to-r from-teal-500 to-teal-600 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-gray-600 text-xs sm:text-sm leading-relaxed">
|
|
Configure SMTP server settings for platform-wide email delivery
|
|
</p>
|
|
<div className="pt-3 sm:pt-4 md:pt-5 border-t border-gray-100">
|
|
<div className="flex items-center justify-between text-xs sm:text-sm">
|
|
<span className="text-gray-500 font-medium">Status</span>
|
|
<span className={`font-bold px-2 sm:px-3 py-1 rounded-lg text-xs sm:text-sm ${
|
|
smtpSettings?.has_host && smtpSettings?.has_user && smtpSettings?.has_password
|
|
? 'text-emerald-600 bg-emerald-50'
|
|
: 'text-gray-400 bg-gray-50'
|
|
}`}>
|
|
{smtpSettings?.has_host && smtpSettings?.has_user && smtpSettings?.has_password ? '✓ Configured' : 'Not configured'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-teal-50/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
|
</div>
|
|
|
|
<div
|
|
onClick={() => setActiveTab('company')}
|
|
className="group relative bg-white/90 backdrop-blur-xl rounded-xl sm:rounded-2xl shadow-xl border border-purple-100/50 p-4 sm:p-6 md:p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-[1.02] sm:hover:scale-105 hover:border-purple-300/60 overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 w-24 h-24 sm:w-32 sm:h-32 bg-gradient-to-br from-purple-400/10 to-transparent rounded-bl-full"></div>
|
|
<div className="relative space-y-3 sm:space-y-4 md:space-y-5">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-2 sm:gap-3 md:gap-4">
|
|
<div className="relative flex-shrink-0">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-purple-400 to-purple-600 rounded-lg sm:rounded-xl blur-md opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
|
<div className="relative p-2 sm:p-2.5 md:p-3.5 rounded-lg sm:rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 shadow-lg border border-purple-400/50 group-hover:scale-110 transition-transform">
|
|
<Building2 className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-gray-900 text-sm sm:text-base md:text-lg mb-1">Company Info</h3>
|
|
<div className="h-0.5 sm:h-1 w-8 sm:w-12 bg-gradient-to-r from-purple-500 to-purple-600 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-gray-600 text-xs sm:text-sm leading-relaxed">
|
|
Manage company branding, logo, favicon, and contact information
|
|
</p>
|
|
<div className="pt-3 sm:pt-4 md:pt-5 border-t border-gray-100">
|
|
<div className="flex items-center justify-between text-xs sm:text-sm">
|
|
<span className="text-gray-500 font-medium">Status</span>
|
|
<span className={`font-bold px-2 sm:px-3 py-1 rounded-lg text-xs sm:text-sm ${
|
|
companySettings?.company_name || companySettings?.company_logo_url
|
|
? 'text-emerald-600 bg-emerald-50'
|
|
: 'text-gray-400 bg-gray-50'
|
|
}`}>
|
|
{companySettings?.company_name || companySettings?.company_logo_url ? '✓ Configured' : 'Not configured'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-purple-50/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{}
|
|
{activeTab === 'cookie' && (
|
|
<div className="space-y-4 sm:space-y-6 md:space-y-8">
|
|
{}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-xl sm:rounded-2xl shadow-xl border border-gray-200/50 p-4 sm:p-6 md:p-8">
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 sm:gap-6">
|
|
<div className="space-y-2 sm:space-y-3">
|
|
<div className="flex items-center gap-2 sm:gap-3">
|
|
<div className="p-2 sm:p-2.5 rounded-lg sm:rounded-xl bg-gradient-to-br from-blue-500/10 to-indigo-500/10 border border-blue-200/40">
|
|
<Shield className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 text-blue-600" />
|
|
</div>
|
|
<h2 className="text-xl sm:text-2xl md:text-2xl font-extrabold text-gray-900">Privacy & Cookie Controls</h2>
|
|
</div>
|
|
<p className="text-gray-600 text-xs sm:text-sm md:text-sm max-w-2xl leading-relaxed">
|
|
Define which cookie categories are allowed in the application. Control user consent preferences and analytics integrations.
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={handleSaveCookie}
|
|
disabled={saving}
|
|
className="group relative px-4 sm:px-6 md:px-8 py-2.5 sm:py-3 md:py-4 bg-gradient-to-r from-amber-500 via-amber-500 to-amber-600 text-white font-semibold rounded-lg sm:rounded-xl shadow-xl shadow-amber-500/30 hover:shadow-2xl hover:shadow-amber-500/40 transition-all duration-300 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 overflow-hidden text-xs sm:text-sm"
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div>
|
|
<div className="relative flex items-center gap-2 sm:gap-3">
|
|
<Save className={`w-4 h-4 sm:w-5 sm:h-5 ${saving ? 'animate-spin' : ''}`} />
|
|
{saving ? 'Saving...' : 'Save Changes'}
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="relative bg-gradient-to-br from-blue-50/80 via-indigo-50/60 to-blue-50/80 backdrop-blur-xl rounded-xl sm:rounded-2xl shadow-xl border border-blue-200/50 p-4 sm:p-6 md:p-8 overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-48 h-48 sm:w-64 sm:h-64 bg-gradient-to-br from-blue-400/20 to-transparent rounded-bl-full"></div>
|
|
<div className="relative flex gap-3 sm:gap-4 md:gap-6">
|
|
<div className="flex-shrink-0">
|
|
<div className="p-2.5 sm:p-3 md:p-4 rounded-lg sm:rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 shadow-lg border border-blue-400/50">
|
|
<Info className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2 sm:space-y-3 flex-1">
|
|
<p className="font-bold text-gray-900 text-sm sm:text-base md:text-base">
|
|
How these settings affect the guest experience
|
|
</p>
|
|
<p className="text-gray-700 text-xs sm:text-sm leading-relaxed">
|
|
Disabling a category here prevents it from being offered to guests as part of the cookie consent flow. For example, if marketing cookies are disabled, the website should not load marketing pixels even if a guest previously opted in.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5 md:gap-6">
|
|
{[
|
|
{ key: 'analytics_enabled' as keyof CookiePolicySettings, label: 'Analytics Cookies', desc: 'Anonymous traffic and performance measurement', color: 'emerald' as const, icon: SlidersHorizontal },
|
|
{ key: 'marketing_enabled' as keyof CookiePolicySettings, label: 'Marketing Cookies', desc: 'Personalised offers and remarketing campaigns', color: 'pink' as const, icon: SlidersHorizontal },
|
|
{ key: 'preferences_enabled' as keyof CookiePolicySettings, label: 'Preference Cookies', desc: 'Remember guest choices like language and currency', color: 'indigo' as const, icon: SlidersHorizontal },
|
|
].map(({ key, label, desc, color, icon: Icon }) => {
|
|
const isEnabled = policy[key];
|
|
const colorClassesMap = {
|
|
emerald: {
|
|
bg: 'from-emerald-500 to-emerald-600',
|
|
shadow: 'shadow-emerald-500/30',
|
|
iconBg: 'bg-emerald-50 border-emerald-100',
|
|
iconColor: 'text-emerald-600',
|
|
},
|
|
pink: {
|
|
bg: 'from-pink-500 to-pink-600',
|
|
shadow: 'shadow-pink-500/30',
|
|
iconBg: 'bg-pink-50 border-pink-100',
|
|
iconColor: 'text-pink-600',
|
|
},
|
|
indigo: {
|
|
bg: 'from-indigo-500 to-indigo-600',
|
|
shadow: 'shadow-indigo-500/30',
|
|
iconBg: 'bg-indigo-50 border-indigo-100',
|
|
iconColor: 'text-indigo-600',
|
|
},
|
|
} as const;
|
|
const colorClasses = colorClassesMap[color] || colorClassesMap.emerald;
|
|
|
|
return (
|
|
<div key={key} className="relative bg-white/90 backdrop-blur-xl rounded-xl sm:rounded-2xl shadow-xl border border-gray-200/50 p-4 sm:p-5 md:p-7 hover:shadow-2xl transition-all duration-300">
|
|
<div className="space-y-3 sm:space-y-4 md:space-y-5">
|
|
<div className="flex items-start justify-between gap-3 sm:gap-4">
|
|
<div className="flex-1 space-y-2 sm:space-y-3">
|
|
<div className="flex items-center gap-2 sm:gap-3">
|
|
<div className={`p-2 sm:p-2.5 rounded-lg ${colorClasses.iconBg} border`}>
|
|
<Icon className={`w-4 h-4 sm:w-5 sm:h-5 ${colorClasses.iconColor}`} />
|
|
</div>
|
|
<p className="font-bold text-gray-900 text-sm sm:text-base md:text-base">{label}</p>
|
|
</div>
|
|
<p className="text-xs sm:text-sm text-gray-600 leading-relaxed">
|
|
{desc}
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleToggle(key)}
|
|
className={`relative inline-flex h-10 w-18 items-center rounded-full transition-all duration-300 shadow-lg ${
|
|
isEnabled
|
|
? `bg-gradient-to-r ${colorClasses.bg} ${colorClasses.shadow}`
|
|
: 'bg-gray-300 shadow-gray-300/20'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-8 w-8 transform rounded-full bg-white shadow-xl transition-all duration-300 ${
|
|
isEnabled ? 'translate-x-9' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{}
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="space-y-8">
|
|
<div className="flex items-start gap-4 pb-6 border-b border-gray-200/60">
|
|
<div className="p-3 rounded-xl bg-gradient-to-br from-blue-500/10 to-indigo-500/10 border border-blue-200/40">
|
|
<Globe className="w-6 h-6 text-blue-600" />
|
|
</div>
|
|
<div className="space-y-2 flex-1">
|
|
<p className="font-bold text-gray-900 text-base sm:text-lg">Third-Party Integrations</p>
|
|
<p className="text-sm text-gray-600 leading-relaxed">
|
|
Configure IDs for supported analytics and marketing platforms. The application will only load these when both the policy and user consent allow it.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-3">
|
|
<label className="block text-sm font-bold text-gray-900 tracking-wide">
|
|
Google Analytics 4 Measurement ID
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={integrations.ga_measurement_id || ''}
|
|
onChange={(e) =>
|
|
setIntegrations((prev) => ({
|
|
...prev,
|
|
ga_measurement_id: e.target.value || undefined,
|
|
}))
|
|
}
|
|
placeholder="G-XXXXXXXXXX"
|
|
className="w-full px-4 py-3 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 leading-relaxed">
|
|
Example: <code className="px-2 py-1 bg-gray-100 rounded text-gray-700 font-mono text-xs">G-ABCDE12345</code>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<label className="block text-sm font-bold text-gray-900 tracking-wide">
|
|
Meta (Facebook) Pixel ID
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={integrations.fb_pixel_id || ''}
|
|
onChange={(e) =>
|
|
setIntegrations((prev) => ({
|
|
...prev,
|
|
fb_pixel_id: e.target.value || undefined,
|
|
}))
|
|
}
|
|
placeholder="123456789012345"
|
|
className="w-full px-4 py-3 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 leading-relaxed">
|
|
Numeric ID from your Meta Pixel configuration
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{}
|
|
{activeTab === 'currency' && (
|
|
<div className="space-y-8">
|
|
{}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2.5 rounded-xl bg-gradient-to-br from-emerald-500/10 to-green-500/10 border border-emerald-200/40">
|
|
<DollarSign className="w-6 h-6 text-emerald-600" />
|
|
</div>
|
|
<h2 className="text-xl sm:text-2xl md:text-2xl font-extrabold text-gray-900">Platform Currency Settings</h2>
|
|
</div>
|
|
<p className="text-gray-600 text-base max-w-2xl leading-relaxed">
|
|
Set the default currency that will be displayed across all dashboards and pages throughout the platform
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={handleSaveCurrency}
|
|
disabled={saving || selectedCurrency === currency}
|
|
className="group relative px-8 py-4 bg-gradient-to-r from-amber-500 via-amber-500 to-amber-600 text-white font-semibold rounded-xl shadow-xl shadow-amber-500/30 hover:shadow-2xl hover:shadow-amber-500/40 transition-all duration-300 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 overflow-hidden"
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div>
|
|
<div className="relative flex items-center gap-3">
|
|
<Save className={`w-5 h-5 ${saving ? 'animate-spin' : ''}`} />
|
|
{saving ? 'Saving...' : 'Save Changes'}
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="relative bg-gradient-to-br from-emerald-50/80 via-green-50/60 to-emerald-50/80 backdrop-blur-xl rounded-2xl shadow-xl border border-emerald-200/50 p-8 overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-emerald-400/20 to-transparent rounded-bl-full"></div>
|
|
<div className="relative flex gap-6">
|
|
<div className="flex-shrink-0">
|
|
<div className="p-4 rounded-xl bg-gradient-to-br from-emerald-500 to-green-600 shadow-lg border border-emerald-400/50">
|
|
<Info className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-3 flex-1">
|
|
<p className="font-bold text-gray-900 text-sm sm:text-base">
|
|
How platform currency works
|
|
</p>
|
|
<p className="text-gray-700 leading-relaxed">
|
|
The platform currency you select here will be used to display all prices, amounts, and financial information across the entire application. This includes customer-facing pages, admin dashboards, reports, and booking pages. All users will see prices in the selected currency.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="space-y-8">
|
|
<div className="flex items-start gap-4 pb-6 border-b border-gray-200/60">
|
|
<div className="p-3 rounded-xl bg-gradient-to-br from-emerald-500/10 to-green-500/10 border border-emerald-200/40">
|
|
<Globe className="w-6 h-6 text-emerald-600" />
|
|
</div>
|
|
<div className="space-y-2 flex-1">
|
|
<p className="font-bold text-gray-900 text-base sm:text-lg">Select Platform Currency</p>
|
|
<p className="text-sm text-gray-600 leading-relaxed">
|
|
Choose the currency that will be used throughout the platform for displaying all monetary values
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4 max-w-2xl">
|
|
<div className="space-y-3">
|
|
<label className="block text-sm font-bold text-gray-900 tracking-wide">
|
|
Currency
|
|
</label>
|
|
<select
|
|
value={selectedCurrency}
|
|
onChange={(e) => setSelectedCurrency(e.target.value)}
|
|
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 font-medium"
|
|
>
|
|
{supportedCurrencies.map((curr) => (
|
|
<option key={curr} value={curr}>
|
|
{curr} - {getCurrencyDisplayName(curr)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<span className="text-gray-500">Current platform currency:</span>
|
|
<span className="font-bold text-emerald-600 bg-emerald-50 px-3 py-1 rounded-lg">{currency}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{}
|
|
{activeTab === 'payment' && (
|
|
<div className="space-y-8">
|
|
{/* Payment Dashboard Header */}
|
|
<div className="bg-gradient-to-br from-slate-50 via-white to-slate-50 rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<div className="p-2.5 rounded-xl bg-gradient-to-br from-amber-500/10 to-amber-600/10 border border-amber-200/40">
|
|
<CreditCard className="w-6 h-6 text-amber-600" />
|
|
</div>
|
|
<h2 className="text-2xl sm:text-3xl font-extrabold bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 bg-clip-text text-transparent">
|
|
Payment Gateway Settings
|
|
</h2>
|
|
</div>
|
|
<p className="text-gray-600 text-base leading-relaxed">
|
|
Choose a payment gateway to configure. Click on any card below to edit its settings.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Payment Method Cards Dashboard */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{/* Stripe Card */}
|
|
<button
|
|
onClick={() => setOpenPaymentModal('stripe')}
|
|
className="group relative bg-gradient-to-br from-indigo-50 via-purple-50 to-indigo-50 rounded-2xl shadow-xl border-2 border-indigo-200/50 hover:border-indigo-400/80 p-8 transition-all duration-300 hover:shadow-2xl hover:scale-[1.02] overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-indigo-400/20 to-transparent rounded-bl-full"></div>
|
|
<div className="relative">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="p-3 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 shadow-lg">
|
|
<CreditCard className="w-8 h-8 text-white" />
|
|
</div>
|
|
{stripeSettings?.has_secret_key && stripeSettings?.has_publishable_key ? (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-100 rounded-full">
|
|
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
|
<span className="text-xs font-semibold text-emerald-700">Configured</span>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-100 rounded-full">
|
|
<span className="text-xs font-semibold text-gray-600">Not Configured</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<h3 className="text-xl font-bold text-gray-900 mb-2">Stripe</h3>
|
|
<p className="text-sm text-gray-600 leading-relaxed mb-4">
|
|
Secure card payment processing with Stripe's powerful API
|
|
</p>
|
|
<div className="flex items-center gap-2 text-sm font-medium text-indigo-600 group-hover:text-indigo-700">
|
|
<span>Configure Settings</span>
|
|
<span className="group-hover:translate-x-1 transition-transform">→</span>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* PayPal Card */}
|
|
<button
|
|
onClick={() => setOpenPaymentModal('paypal')}
|
|
className="group relative bg-gradient-to-br from-blue-50 via-cyan-50 to-blue-50 rounded-2xl shadow-xl border-2 border-blue-200/50 hover:border-blue-400/80 p-8 transition-all duration-300 hover:shadow-2xl hover:scale-[1.02] overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-blue-400/20 to-transparent rounded-bl-full"></div>
|
|
<div className="relative">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="p-3 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-600 shadow-lg">
|
|
<CreditCard className="w-8 h-8 text-white" />
|
|
</div>
|
|
{paypalSettings?.has_client_id && paypalSettings?.has_client_secret ? (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-100 rounded-full">
|
|
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
|
<span className="text-xs font-semibold text-emerald-700">Configured</span>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-100 rounded-full">
|
|
<span className="text-xs font-semibold text-gray-600">Not Configured</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<h3 className="text-xl font-bold text-gray-900 mb-2">PayPal</h3>
|
|
<p className="text-sm text-gray-600 leading-relaxed mb-4">
|
|
Enable PayPal payments for your customers worldwide
|
|
</p>
|
|
<div className="flex items-center gap-2 text-sm font-medium text-blue-600 group-hover:text-blue-700">
|
|
<span>Configure Settings</span>
|
|
<span className="group-hover:translate-x-1 transition-transform">→</span>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Borica Card */}
|
|
<button
|
|
onClick={() => setOpenPaymentModal('borica')}
|
|
className="group relative bg-gradient-to-br from-purple-50 via-indigo-50 to-purple-50 rounded-2xl shadow-xl border-2 border-purple-200/50 hover:border-purple-400/80 p-8 transition-all duration-300 hover:shadow-2xl hover:scale-[1.02] overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-purple-400/20 to-transparent rounded-bl-full"></div>
|
|
<div className="relative">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="p-3 rounded-xl bg-gradient-to-br from-purple-500 to-indigo-600 shadow-lg">
|
|
<CreditCard className="w-8 h-8 text-white" />
|
|
</div>
|
|
{boricaSettings?.has_terminal_id && boricaSettings?.has_merchant_id ? (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-100 rounded-full">
|
|
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
|
<span className="text-xs font-semibold text-emerald-700">Configured</span>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-100 rounded-full">
|
|
<span className="text-xs font-semibold text-gray-600">Not Configured</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<h3 className="text-xl font-bold text-gray-900 mb-2">Borica</h3>
|
|
<p className="text-sm text-gray-600 leading-relaxed mb-4">
|
|
Bulgarian payment gateway system for local payments
|
|
</p>
|
|
<div className="flex items-center gap-2 text-sm font-medium text-purple-600 group-hover:text-purple-700">
|
|
<span>Configure Settings</span>
|
|
<span className="group-hover:translate-x-1 transition-transform">→</span>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Payment Modals */}
|
|
{openPaymentModal === 'stripe' && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center" aria-labelledby="modal-title" role="dialog" aria-modal="true" style={{ overflow: 'hidden' }}>
|
|
<div
|
|
className="fixed inset-0 bg-gradient-to-br from-black/70 via-black/60 to-black/70 backdrop-blur-md transition-opacity animate-fade-in"
|
|
onClick={() => setOpenPaymentModal(null)}
|
|
aria-hidden="true"
|
|
/>
|
|
<div className="relative w-full max-w-4xl mx-4 my-4 max-h-[90vh] flex flex-col">
|
|
<div className="relative transform overflow-hidden rounded-2xl bg-gradient-to-br from-white via-gray-50/50 to-white text-left shadow-2xl border border-gray-200/80 transition-all animate-scale-in flex flex-col max-h-full">
|
|
{/* Header */}
|
|
<div className="relative bg-gradient-to-r from-indigo-600 via-purple-600 to-indigo-700 px-4 sm:px-5 py-3.5 sm:py-4 flex items-center justify-between shadow-lg flex-shrink-0">
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent opacity-0 hover:opacity-100 transition-opacity"></div>
|
|
<div className="relative flex items-center gap-2.5 sm:gap-3">
|
|
<div className="p-2 rounded-lg bg-white/20 backdrop-blur-sm border border-white/30 shadow-lg">
|
|
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-base sm:text-lg md:text-xl font-extrabold text-white tracking-tight">Stripe Payment Settings</h3>
|
|
<p className="text-xs text-white/80 hidden sm:block">Configure your Stripe integration</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setOpenPaymentModal(null)}
|
|
className="relative p-1.5 rounded-lg bg-white/10 hover:bg-white/20 backdrop-blur-sm border border-white/20 text-white/90 hover:text-white transition-all duration-200 hover:scale-110"
|
|
aria-label="Close modal"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto custom-scrollbar px-4 sm:px-5 py-4 sm:py-5 space-y-4">
|
|
{/* Info Box - Compact */}
|
|
<div className="relative bg-gradient-to-br from-indigo-50/90 via-purple-50/70 to-indigo-50/90 backdrop-blur-xl rounded-xl shadow-lg border border-indigo-200/60 p-3 sm:p-4 overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-24 h-24 bg-gradient-to-br from-indigo-400/20 to-transparent rounded-bl-full"></div>
|
|
<div className="relative flex gap-3">
|
|
<div className="flex-shrink-0">
|
|
<div className="p-2 rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 shadow-md">
|
|
<Info className="w-4 h-4 text-white" />
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-bold text-sm text-gray-900 mb-1">How Stripe payments work</p>
|
|
<p className="text-xs text-gray-600 leading-relaxed">
|
|
Provide your Stripe API keys from your Stripe Dashboard. Leave fields empty to keep existing values.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Form Fields */}
|
|
<div className="space-y-3.5 sm:space-y-4">
|
|
<div className="space-y-1.5 sm:space-y-2">
|
|
<label className="flex items-center gap-1.5 text-xs sm:text-sm font-bold text-gray-900">
|
|
<div className="p-1 rounded-md bg-indigo-100">
|
|
<Lock className="w-3 h-3 text-indigo-600" />
|
|
</div>
|
|
Stripe Secret Key <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative group">
|
|
<input
|
|
type={showSecretKey ? 'text' : 'password'}
|
|
value={formData.stripe_secret_key}
|
|
onChange={(e) => setFormData({ ...formData, stripe_secret_key: e.target.value })}
|
|
placeholder={stripeSettings?.has_secret_key ? `Current: ${stripeSettings.stripe_secret_key_masked || '****'}` : 'sk_test_... or sk_live_...'}
|
|
className="w-full px-3 sm:px-4 py-2.5 sm:py-3 pr-10 bg-white border-2 border-gray-200 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-xs sm:text-sm font-mono group-hover:border-gray-300"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowSecretKey(!showSecretKey)}
|
|
className="absolute right-2.5 sm:right-3 top-1/2 -translate-y-1/2 p-1 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 transition-all duration-200"
|
|
aria-label={showSecretKey ? 'Hide secret key' : 'Show secret key'}
|
|
>
|
|
{showSecretKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
<p className="text-xs text-gray-500">Must start with <code className="px-1.5 py-0.5 bg-gray-100 rounded text-gray-700 font-mono">sk_</code></p>
|
|
</div>
|
|
|
|
<div className="space-y-1.5 sm:space-y-2">
|
|
<label className="flex items-center gap-1.5 text-xs sm:text-sm font-bold text-gray-900">
|
|
<div className="p-1 rounded-md bg-indigo-100">
|
|
<Globe className="w-3 h-3 text-indigo-600" />
|
|
</div>
|
|
Stripe Publishable Key <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative group">
|
|
<input
|
|
type="text"
|
|
value={formData.stripe_publishable_key}
|
|
onChange={(e) => setFormData({ ...formData, stripe_publishable_key: e.target.value })}
|
|
placeholder="pk_test_... or pk_live_..."
|
|
className="w-full px-3 sm:px-4 py-2.5 sm:py-3 bg-white border-2 border-gray-200 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-xs sm:text-sm font-mono group-hover:border-gray-300"
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-gray-500">Must start with <code className="px-1.5 py-0.5 bg-gray-100 rounded text-gray-700 font-mono">pk_</code></p>
|
|
</div>
|
|
|
|
<div className="space-y-1.5 sm:space-y-2">
|
|
<label className="flex items-center gap-1.5 text-xs sm:text-sm font-bold text-gray-900">
|
|
<div className="p-1 rounded-md bg-indigo-100">
|
|
<Lock className="w-3 h-3 text-indigo-600" />
|
|
</div>
|
|
Stripe Webhook Secret
|
|
</label>
|
|
<div className="relative group">
|
|
<input
|
|
type={showWebhookSecret ? 'text' : 'password'}
|
|
value={formData.stripe_webhook_secret}
|
|
onChange={(e) => setFormData({ ...formData, stripe_webhook_secret: e.target.value })}
|
|
placeholder={stripeSettings?.has_webhook_secret ? `Current: ${stripeSettings.stripe_webhook_secret_masked || '****'}` : 'whsec_...'}
|
|
className="w-full px-3 sm:px-4 py-2.5 sm:py-3 pr-10 bg-white border-2 border-gray-200 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-xs sm:text-sm font-mono group-hover:border-gray-300"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowWebhookSecret(!showWebhookSecret)}
|
|
className="absolute right-2.5 sm:right-3 top-1/2 -translate-y-1/2 p-1 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 transition-all duration-200"
|
|
aria-label={showWebhookSecret ? 'Hide webhook secret' : 'Show webhook secret'}
|
|
>
|
|
{showWebhookSecret ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
<p className="text-xs text-gray-500">Must start with <code className="px-1.5 py-0.5 bg-gray-100 rounded text-gray-700 font-mono">whsec_</code></p>
|
|
</div>
|
|
|
|
<div className="relative bg-gradient-to-br from-yellow-50/90 to-amber-50/90 border border-yellow-200/60 rounded-xl p-3 shadow-md overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-20 h-20 bg-gradient-to-br from-yellow-400/20 to-transparent rounded-bl-full"></div>
|
|
<div className="relative">
|
|
<div className="flex items-center gap-1.5 mb-2">
|
|
<Info className="w-3.5 h-3.5 text-yellow-700" />
|
|
<p className="text-xs font-bold text-yellow-900">Webhook Endpoint URL</p>
|
|
</div>
|
|
<code className="block text-xs bg-yellow-100/80 px-2.5 py-2 rounded-lg text-yellow-900 break-all font-mono border border-yellow-200/60 mb-1.5">{window.location.origin}/api/payments/stripe/webhook</code>
|
|
<p className="text-xs text-yellow-800">Configure in <a href="https://dashboard.stripe.com/webhooks" target="_blank" rel="noopener noreferrer" className="underline font-semibold hover:text-yellow-900">Stripe Webhooks Dashboard</a></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Footer */}
|
|
<div className="bg-gradient-to-r from-gray-50 via-white to-gray-50 px-4 sm:px-5 py-3 sm:py-3.5 flex flex-col sm:flex-row justify-end gap-2.5 sm:gap-3 border-t border-gray-200/60 flex-shrink-0">
|
|
<button
|
|
onClick={() => setOpenPaymentModal(null)}
|
|
className="w-full sm:w-auto px-5 sm:px-6 py-2.5 sm:py-2.5 bg-white border-2 border-gray-300 rounded-lg text-gray-700 font-semibold hover:bg-gray-50 hover:border-gray-400 transition-all duration-200 shadow-sm hover:shadow-md text-sm"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleSaveStripe}
|
|
disabled={saving}
|
|
className="group relative w-full sm:w-auto px-5 sm:px-6 py-2.5 sm:py-2.5 bg-gradient-to-r from-amber-500 via-amber-600 to-amber-500 text-white font-bold rounded-lg shadow-lg hover:shadow-xl transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed overflow-hidden text-sm"
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div>
|
|
<span className="relative flex items-center justify-center gap-2">
|
|
{saving ? (
|
|
<>
|
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
|
Saving...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="w-3.5 h-3.5" />
|
|
Save Changes
|
|
</>
|
|
)}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{openPaymentModal === 'paypal' && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center" aria-labelledby="modal-title" role="dialog" aria-modal="true" style={{ overflow: 'hidden' }}>
|
|
<div
|
|
className="fixed inset-0 bg-gradient-to-br from-black/70 via-black/60 to-black/70 backdrop-blur-md transition-opacity animate-fade-in"
|
|
onClick={() => setOpenPaymentModal(null)}
|
|
aria-hidden="true"
|
|
/>
|
|
<div className="relative w-full max-w-4xl mx-4 my-4 max-h-[90vh] flex flex-col">
|
|
<div className="relative transform overflow-hidden rounded-2xl bg-gradient-to-br from-white via-gray-50/50 to-white text-left shadow-2xl border border-gray-200/80 transition-all animate-scale-in flex flex-col max-h-full">
|
|
{/* Header */}
|
|
<div className="relative bg-gradient-to-r from-blue-600 via-cyan-600 to-blue-700 px-4 sm:px-5 py-3.5 sm:py-4 flex items-center justify-between shadow-lg flex-shrink-0">
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent opacity-0 hover:opacity-100 transition-opacity"></div>
|
|
<div className="relative flex items-center gap-2.5 sm:gap-3">
|
|
<div className="p-2 rounded-lg bg-white/20 backdrop-blur-sm border border-white/30 shadow-lg">
|
|
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-base sm:text-lg md:text-xl font-extrabold text-white tracking-tight">PayPal Payment Settings</h3>
|
|
<p className="text-xs text-white/80 hidden sm:block">Configure your PayPal integration</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setOpenPaymentModal(null)}
|
|
className="relative p-1.5 rounded-lg bg-white/10 hover:bg-white/20 backdrop-blur-sm border border-white/20 text-white/90 hover:text-white transition-all duration-200 hover:scale-110"
|
|
aria-label="Close modal"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto custom-scrollbar px-4 sm:px-5 py-4 sm:py-5 space-y-4">
|
|
{/* Info Box - Compact */}
|
|
<div className="relative bg-gradient-to-br from-blue-50/90 via-cyan-50/70 to-blue-50/90 backdrop-blur-xl rounded-xl shadow-lg border border-blue-200/60 p-3 sm:p-4 overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-24 h-24 bg-gradient-to-br from-blue-400/20 to-transparent rounded-bl-full"></div>
|
|
<div className="relative flex gap-3">
|
|
<div className="flex-shrink-0">
|
|
<div className="p-2 rounded-lg bg-gradient-to-br from-blue-500 to-cyan-600 shadow-md">
|
|
<Info className="w-4 h-4 text-white" />
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-bold text-sm text-gray-900 mb-1">How PayPal payments work</p>
|
|
<p className="text-xs text-gray-600 leading-relaxed">
|
|
Provide your PayPal API credentials from your PayPal Developer Dashboard. Leave fields empty to keep existing values.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Form Fields */}
|
|
<div className="space-y-3.5 sm:space-y-4">
|
|
<div className="space-y-1.5 sm:space-y-2">
|
|
<label className="flex items-center gap-1.5 text-xs sm:text-sm font-bold text-gray-900">
|
|
<div className="p-1 rounded-md bg-blue-100">
|
|
<Globe className="w-3 h-3 text-blue-600" />
|
|
</div>
|
|
PayPal Client ID <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative group">
|
|
<input
|
|
type="text"
|
|
value={paypalFormData.paypal_client_id}
|
|
onChange={(e) => setPaypalFormData({ ...paypalFormData, paypal_client_id: e.target.value })}
|
|
placeholder={paypalSettings?.has_client_id ? `Current: ${paypalSettings.paypal_client_id || '****'}` : 'Client ID from PayPal Dashboard'}
|
|
className="w-full px-3 sm:px-4 py-2.5 sm:py-3 bg-white border-2 border-gray-200 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-xs sm:text-sm group-hover:border-gray-300"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5 sm:space-y-2">
|
|
<label className="flex items-center gap-1.5 text-xs sm:text-sm font-bold text-gray-900">
|
|
<div className="p-1 rounded-md bg-blue-100">
|
|
<Lock className="w-3 h-3 text-blue-600" />
|
|
</div>
|
|
PayPal Client Secret <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative group">
|
|
<input
|
|
type={showPayPalSecret ? 'text' : 'password'}
|
|
value={paypalFormData.paypal_client_secret}
|
|
onChange={(e) => setPaypalFormData({ ...paypalFormData, paypal_client_secret: e.target.value })}
|
|
placeholder={paypalSettings?.has_client_secret ? `Current: ${paypalSettings.paypal_client_secret_masked || '****'}` : 'Client Secret from PayPal Dashboard'}
|
|
className="w-full px-3 sm:px-4 py-2.5 sm:py-3 pr-10 bg-white border-2 border-gray-200 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-xs sm:text-sm font-mono group-hover:border-gray-300"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPayPalSecret(!showPayPalSecret)}
|
|
className="absolute right-2.5 sm:right-3 top-1/2 -translate-y-1/2 p-1 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 transition-all duration-200"
|
|
aria-label={showPayPalSecret ? 'Hide client secret' : 'Show client secret'}
|
|
>
|
|
{showPayPalSecret ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5 sm:space-y-2">
|
|
<label className="flex items-center gap-1.5 text-xs sm:text-sm font-bold text-gray-900">
|
|
<div className="p-1 rounded-md bg-blue-100">
|
|
<Globe className="w-3 h-3 text-blue-600" />
|
|
</div>
|
|
PayPal Mode <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative group">
|
|
<select
|
|
value={paypalFormData.paypal_mode}
|
|
onChange={(e) => setPaypalFormData({ ...paypalFormData, paypal_mode: e.target.value })}
|
|
className="w-full px-3 sm:px-4 py-2.5 sm:py-3 bg-white border-2 border-gray-200 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-xs sm:text-sm appearance-none cursor-pointer group-hover:border-gray-300"
|
|
>
|
|
<option value="sandbox">Sandbox (Testing)</option>
|
|
<option value="live">Live (Production)</option>
|
|
</select>
|
|
</div>
|
|
<p className="text-xs text-gray-500">Use sandbox mode for testing with test credentials, or live mode for production payments.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Footer */}
|
|
<div className="bg-gradient-to-r from-gray-50 via-white to-gray-50 px-4 sm:px-5 py-3 sm:py-3.5 flex flex-col sm:flex-row justify-end gap-2.5 sm:gap-3 border-t border-gray-200/60 flex-shrink-0">
|
|
<button
|
|
onClick={() => setOpenPaymentModal(null)}
|
|
className="w-full sm:w-auto px-5 sm:px-6 py-2.5 bg-white border-2 border-gray-300 rounded-lg text-gray-700 font-semibold hover:bg-gray-50 hover:border-gray-400 transition-all duration-200 shadow-sm hover:shadow-md text-sm"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleSavePayPal}
|
|
disabled={saving}
|
|
className="group relative w-full sm:w-auto px-5 sm:px-6 py-2.5 bg-gradient-to-r from-amber-500 via-amber-600 to-amber-500 text-white font-bold rounded-lg shadow-lg hover:shadow-xl transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed overflow-hidden text-sm"
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div>
|
|
<span className="relative flex items-center justify-center gap-2">
|
|
{saving ? (
|
|
<>
|
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
|
Saving...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="w-3.5 h-3.5" />
|
|
Save Changes
|
|
</>
|
|
)}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{openPaymentModal === 'borica' && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center" aria-labelledby="modal-title" role="dialog" aria-modal="true" style={{ overflow: 'hidden' }}>
|
|
<div
|
|
className="fixed inset-0 bg-gradient-to-br from-black/70 via-black/60 to-black/70 backdrop-blur-md transition-opacity animate-fade-in"
|
|
onClick={() => setOpenPaymentModal(null)}
|
|
aria-hidden="true"
|
|
/>
|
|
<div className="relative w-full max-w-4xl mx-4 my-4 max-h-[90vh] flex flex-col">
|
|
<div className="relative transform overflow-hidden rounded-2xl bg-gradient-to-br from-white via-gray-50/50 to-white text-left shadow-2xl border border-gray-200/80 transition-all animate-scale-in flex flex-col max-h-full">
|
|
{/* Header */}
|
|
<div className="relative bg-gradient-to-r from-purple-600 via-indigo-600 to-purple-700 px-4 sm:px-5 py-3.5 sm:py-4 flex items-center justify-between shadow-lg flex-shrink-0">
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent opacity-0 hover:opacity-100 transition-opacity"></div>
|
|
<div className="relative flex items-center gap-2.5 sm:gap-3">
|
|
<div className="p-2 rounded-lg bg-white/20 backdrop-blur-sm border border-white/30 shadow-lg">
|
|
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-base sm:text-lg md:text-xl font-extrabold text-white tracking-tight">Borica Payment Settings</h3>
|
|
<p className="text-xs text-white/80 hidden sm:block">Configure your Borica integration</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setOpenPaymentModal(null)}
|
|
className="relative p-1.5 rounded-lg bg-white/10 hover:bg-white/20 backdrop-blur-sm border border-white/20 text-white/90 hover:text-white transition-all duration-200 hover:scale-110"
|
|
aria-label="Close modal"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto custom-scrollbar px-4 sm:px-5 py-4 sm:py-5 space-y-4">
|
|
{/* Info Box - Compact */}
|
|
<div className="relative bg-gradient-to-br from-purple-50/90 via-indigo-50/70 to-purple-50/90 backdrop-blur-xl rounded-xl shadow-lg border border-purple-200/60 p-3 sm:p-4 overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-24 h-24 bg-gradient-to-br from-purple-400/20 to-transparent rounded-bl-full"></div>
|
|
<div className="relative flex gap-3">
|
|
<div className="flex-shrink-0">
|
|
<div className="p-2 rounded-lg bg-gradient-to-br from-purple-500 to-indigo-600 shadow-md">
|
|
<Info className="w-4 h-4 text-white" />
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-bold text-sm text-gray-900 mb-1">How Borica payments work</p>
|
|
<p className="text-xs text-gray-600 leading-relaxed">
|
|
Provide your Terminal ID, Merchant ID, and certificate files from your Borica merchant account. Leave fields empty to keep existing values. Certificate paths should be absolute paths.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Form Fields */}
|
|
<div className="space-y-3.5 sm:space-y-4">
|
|
<div className="space-y-1.5 sm:space-y-2">
|
|
<label className="flex items-center gap-1.5 text-xs sm:text-sm font-bold text-gray-900">
|
|
<div className="p-1 rounded-md bg-purple-100">
|
|
<Globe className="w-3 h-3 text-purple-600" />
|
|
</div>
|
|
Terminal ID <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative group">
|
|
<input
|
|
type={showBoricaTerminalId ? 'text' : 'password'}
|
|
value={boricaFormData.borica_terminal_id}
|
|
onChange={(e) => setBoricaFormData({ ...boricaFormData, borica_terminal_id: e.target.value })}
|
|
placeholder={boricaSettings?.has_terminal_id ? `Current: ${boricaSettings.borica_terminal_id_masked || '****'}` : 'Terminal ID from Borica'}
|
|
className="w-full px-3 sm:px-4 py-2.5 sm:py-3 pr-10 bg-white border-2 border-gray-200 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-xs sm:text-sm font-mono group-hover:border-gray-300"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowBoricaTerminalId(!showBoricaTerminalId)}
|
|
className="absolute right-2.5 sm:right-3 top-1/2 -translate-y-1/2 p-1 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 transition-all duration-200"
|
|
aria-label={showBoricaTerminalId ? 'Hide terminal ID' : 'Show terminal ID'}
|
|
>
|
|
{showBoricaTerminalId ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5 sm:space-y-2">
|
|
<label className="flex items-center gap-1.5 text-xs sm:text-sm font-bold text-gray-900">
|
|
<div className="p-1 rounded-md bg-purple-100">
|
|
<Globe className="w-3 h-3 text-purple-600" />
|
|
</div>
|
|
Merchant ID <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative group">
|
|
<input
|
|
type={showBoricaMerchantId ? 'text' : 'password'}
|
|
value={boricaFormData.borica_merchant_id}
|
|
onChange={(e) => setBoricaFormData({ ...boricaFormData, borica_merchant_id: e.target.value })}
|
|
placeholder={boricaSettings?.has_merchant_id ? `Current: ${boricaSettings.borica_merchant_id_masked || '****'}` : 'Merchant ID from Borica'}
|
|
className="w-full px-3 sm:px-4 py-2.5 sm:py-3 pr-10 bg-white border-2 border-gray-200 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-xs sm:text-sm font-mono group-hover:border-gray-300"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowBoricaMerchantId(!showBoricaMerchantId)}
|
|
className="absolute right-2.5 sm:right-3 top-1/2 -translate-y-1/2 p-1 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 transition-all duration-200"
|
|
aria-label={showBoricaMerchantId ? 'Hide merchant ID' : 'Show merchant ID'}
|
|
>
|
|
{showBoricaMerchantId ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5 sm:space-y-2">
|
|
<label className="flex items-center gap-1.5 text-xs sm:text-sm font-bold text-gray-900">
|
|
<div className="p-1 rounded-md bg-purple-100">
|
|
<Lock className="w-3 h-3 text-purple-600" />
|
|
</div>
|
|
Private Key Path <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="flex flex-col sm:flex-row gap-2">
|
|
<div className="relative flex-1 group">
|
|
<input
|
|
type="text"
|
|
value={boricaFormData.borica_private_key_path}
|
|
onChange={(e) => setBoricaFormData({ ...boricaFormData, borica_private_key_path: e.target.value })}
|
|
placeholder={boricaSettings?.has_private_key_path ? `Current: ${boricaSettings.borica_private_key_path || 'Not set'}` : '/path/to/private_key.pem'}
|
|
className="w-full px-3 sm:px-4 py-2.5 sm:py-3 bg-white border-2 border-gray-200 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-xs sm:text-sm font-mono group-hover:border-gray-300"
|
|
/>
|
|
</div>
|
|
<input
|
|
type="file"
|
|
ref={privateKeyFileInputRef}
|
|
accept=".pem,.key,.crt,.cer,.p12,.pfx"
|
|
onChange={(e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
handleUploadBoricaFile(file, 'private_key');
|
|
}
|
|
e.target.value = '';
|
|
}}
|
|
className="hidden"
|
|
disabled={uploadingPrivateKey}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => privateKeyFileInputRef.current?.click()}
|
|
disabled={uploadingPrivateKey}
|
|
className="px-4 sm:px-5 py-2.5 sm:py-3 bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-600 hover:to-amber-700 disabled:from-gray-400 disabled:to-gray-500 text-white font-semibold rounded-lg shadow-md hover:shadow-lg transition-all duration-200 flex items-center justify-center gap-1.5 whitespace-nowrap text-xs sm:text-sm"
|
|
>
|
|
{uploadingPrivateKey ? (
|
|
<>
|
|
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
|
<span className="hidden sm:inline">Uploading...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Upload className="w-3.5 h-3.5" />
|
|
<span className="hidden sm:inline">Upload</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
<p className="text-xs text-gray-500">Upload file (.pem, .key, .crt, .cer, .p12, .pfx) or enter absolute path</p>
|
|
</div>
|
|
|
|
<div className="space-y-1.5 sm:space-y-2">
|
|
<label className="flex items-center gap-1.5 text-xs sm:text-sm font-bold text-gray-900">
|
|
<div className="p-1 rounded-md bg-purple-100">
|
|
<Lock className="w-3 h-3 text-purple-600" />
|
|
</div>
|
|
Certificate Path <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="flex flex-col sm:flex-row gap-2">
|
|
<div className="relative flex-1 group">
|
|
<input
|
|
type="text"
|
|
value={boricaFormData.borica_certificate_path}
|
|
onChange={(e) => setBoricaFormData({ ...boricaFormData, borica_certificate_path: e.target.value })}
|
|
placeholder={boricaSettings?.has_certificate_path ? `Current: ${boricaSettings.borica_certificate_path || 'Not set'}` : '/path/to/certificate.pem'}
|
|
className="w-full px-3 sm:px-4 py-2.5 sm:py-3 bg-white border-2 border-gray-200 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-xs sm:text-sm font-mono group-hover:border-gray-300"
|
|
/>
|
|
</div>
|
|
<input
|
|
type="file"
|
|
ref={certificateFileInputRef}
|
|
accept=".pem,.key,.crt,.cer,.p12,.pfx"
|
|
onChange={(e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
handleUploadBoricaFile(file, 'certificate');
|
|
}
|
|
e.target.value = '';
|
|
}}
|
|
className="hidden"
|
|
disabled={uploadingCertificate}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => certificateFileInputRef.current?.click()}
|
|
disabled={uploadingCertificate}
|
|
className="px-4 sm:px-5 py-2.5 sm:py-3 bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-600 hover:to-amber-700 disabled:from-gray-400 disabled:to-gray-500 text-white font-semibold rounded-lg shadow-md hover:shadow-lg transition-all duration-200 flex items-center justify-center gap-1.5 whitespace-nowrap text-xs sm:text-sm"
|
|
>
|
|
{uploadingCertificate ? (
|
|
<>
|
|
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
|
<span className="hidden sm:inline">Uploading...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Upload className="w-3.5 h-3.5" />
|
|
<span className="hidden sm:inline">Upload</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
<p className="text-xs text-gray-500">Upload file (.pem, .key, .crt, .cer, .p12, .pfx) or enter absolute path</p>
|
|
</div>
|
|
|
|
<div className="space-y-1.5 sm:space-y-2">
|
|
<label className="flex items-center gap-1.5 text-xs sm:text-sm font-bold text-gray-900">
|
|
<div className="p-1 rounded-md bg-purple-100">
|
|
<Globe className="w-3 h-3 text-purple-600" />
|
|
</div>
|
|
Gateway URL
|
|
</label>
|
|
<div className="relative group">
|
|
<input
|
|
type="text"
|
|
value={boricaFormData.borica_gateway_url}
|
|
onChange={(e) => setBoricaFormData({ ...boricaFormData, borica_gateway_url: e.target.value })}
|
|
placeholder={boricaSettings?.borica_gateway_url ? `Current: ${boricaSettings.borica_gateway_url}` : 'https://3dsgate-dev.borica.bg/cgi-bin/cgi_link'}
|
|
className="w-full px-3 sm:px-4 py-2.5 sm:py-3 bg-white border-2 border-gray-200 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-xs sm:text-sm group-hover:border-gray-300"
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-gray-500">Test: <code className="px-1 py-0.5 bg-gray-100 rounded text-gray-700 font-mono">https://3dsgate-dev.borica.bg/...</code> | Prod: <code className="px-1 py-0.5 bg-gray-100 rounded text-gray-700 font-mono">https://3dsgate.borica.bg/...</code></p>
|
|
</div>
|
|
|
|
<div className="space-y-1.5 sm:space-y-2">
|
|
<label className="flex items-center gap-1.5 text-xs sm:text-sm font-bold text-gray-900">
|
|
<div className="p-1 rounded-md bg-purple-100">
|
|
<Globe className="w-3 h-3 text-purple-600" />
|
|
</div>
|
|
Mode <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative group">
|
|
<select
|
|
value={boricaFormData.borica_mode}
|
|
onChange={(e) => setBoricaFormData({ ...boricaFormData, borica_mode: e.target.value })}
|
|
className="w-full px-3 sm:px-4 py-2.5 sm:py-3 bg-white border-2 border-gray-200 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-xs sm:text-sm appearance-none cursor-pointer group-hover:border-gray-300"
|
|
>
|
|
<option value="test">Test</option>
|
|
<option value="production">Production</option>
|
|
</select>
|
|
</div>
|
|
<p className="text-xs text-gray-500">Use test mode for testing or production mode for live payments.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Footer */}
|
|
<div className="bg-gradient-to-r from-gray-50 via-white to-gray-50 px-4 sm:px-5 py-3 sm:py-3.5 flex flex-col sm:flex-row justify-end gap-2.5 sm:gap-3 border-t border-gray-200/60 flex-shrink-0">
|
|
<button
|
|
onClick={() => setOpenPaymentModal(null)}
|
|
className="w-full sm:w-auto px-5 sm:px-6 py-2.5 bg-white border-2 border-gray-300 rounded-lg text-gray-700 font-semibold hover:bg-gray-50 hover:border-gray-400 transition-all duration-200 shadow-sm hover:shadow-md text-sm"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleSaveBorica}
|
|
disabled={saving}
|
|
className="group relative w-full sm:w-auto px-5 sm:px-6 py-2.5 bg-gradient-to-r from-amber-500 via-amber-600 to-amber-500 text-white font-bold rounded-lg shadow-lg hover:shadow-xl transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed overflow-hidden text-sm"
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div>
|
|
<span className="relative flex items-center justify-center gap-2">
|
|
{saving ? (
|
|
<>
|
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
|
Saving...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="w-3.5 h-3.5" />
|
|
Save Changes
|
|
</>
|
|
)}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{}
|
|
{activeTab === 'smtp' && (
|
|
<div className="space-y-8">
|
|
{}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2.5 rounded-xl bg-gradient-to-br from-teal-500/10 to-cyan-500/10 border border-teal-200/40">
|
|
<Mail className="w-6 h-6 text-teal-600" />
|
|
</div>
|
|
<h2 className="text-xl sm:text-2xl md:text-2xl font-extrabold text-gray-900">SMTP Email Server Settings</h2>
|
|
</div>
|
|
<p className="text-gray-600 text-base max-w-2xl leading-relaxed">
|
|
Configure your SMTP server settings for sending emails. These settings will be used platform-wide for all outgoing emails.
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={handleSaveSmtp}
|
|
disabled={saving}
|
|
className="group relative px-8 py-4 bg-gradient-to-r from-amber-500 via-amber-500 to-amber-600 text-white font-semibold rounded-xl shadow-xl shadow-amber-500/30 hover:shadow-2xl hover:shadow-amber-500/40 transition-all duration-300 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 overflow-hidden"
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div>
|
|
<div className="relative flex items-center gap-3">
|
|
<Save className={`w-5 h-5 ${saving ? 'animate-spin' : ''}`} />
|
|
{saving ? 'Saving...' : 'Save Changes'}
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-start gap-4 mb-6">
|
|
<div className="p-3 rounded-xl bg-gradient-to-br from-teal-500/10 to-cyan-500/10 border border-teal-200/40">
|
|
<Mail className="w-6 h-6 text-teal-600" />
|
|
</div>
|
|
<div className="space-y-2 flex-1">
|
|
<h3 className="text-base sm:text-lg md:text-lg font-bold text-gray-900">Test Email Configuration</h3>
|
|
<p className="text-sm text-gray-600 leading-relaxed">
|
|
Send a test email to verify that your SMTP settings are working correctly.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
<div className="flex-1">
|
|
<label className="block text-sm font-semibold text-gray-900 mb-2 tracking-wide">
|
|
Test Email Address
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={testEmailAddress}
|
|
onChange={(e) => setTestEmailAddress(e.target.value)}
|
|
placeholder="Enter email address to send test email"
|
|
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-teal-500/50 focus:border-teal-500 transition-all duration-200 text-sm"
|
|
disabled={testingEmail}
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
Enter the email address where you want to receive the test email
|
|
</p>
|
|
</div>
|
|
<div className="flex items-end">
|
|
<button
|
|
type="button"
|
|
onClick={handleTestEmail}
|
|
disabled={testingEmail || !testEmailAddress.trim()}
|
|
className="group relative px-8 py-3.5 bg-gradient-to-r from-teal-500 via-teal-500 to-teal-600 text-white font-semibold rounded-xl shadow-xl shadow-teal-500/30 hover:shadow-2xl hover:shadow-teal-500/40 transition-all duration-300 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 overflow-hidden"
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div>
|
|
<div className="relative flex items-center gap-3">
|
|
<Mail className={`w-5 h-5 ${testingEmail ? 'animate-spin' : ''}`} />
|
|
{testingEmail ? 'Sending...' : 'Send Test Email'}
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="relative bg-gradient-to-br from-teal-50/80 via-cyan-50/60 to-teal-50/80 backdrop-blur-xl rounded-2xl shadow-xl border border-teal-200/50 p-8 overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-teal-400/20 to-transparent rounded-bl-full"></div>
|
|
<div className="relative flex gap-6">
|
|
<div className="flex-shrink-0">
|
|
<div className="p-4 rounded-xl bg-gradient-to-br from-teal-500 to-cyan-600 shadow-lg border border-teal-400/50">
|
|
<Info className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-3 flex-1">
|
|
<p className="font-bold text-gray-900 text-sm sm:text-base">
|
|
How SMTP settings work
|
|
</p>
|
|
<p className="text-gray-700 leading-relaxed">
|
|
The SMTP server configured here will be used for all outgoing emails across the platform, including welcome emails, password resets, booking confirmations, and notifications. Common SMTP services include Gmail, SendGrid, Mailgun, and AWS SES.
|
|
</p>
|
|
<div className="pt-3 border-t border-teal-200/50">
|
|
<p className="text-sm text-gray-600">
|
|
<strong className="text-gray-900">Note:</strong> Leave fields empty to keep existing values. Only enter new values when you want to update them. The password field will be masked for security.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="space-y-8">
|
|
<div className="flex items-start gap-4 pb-6 border-b border-gray-200/60">
|
|
<div className="p-3 rounded-xl bg-gradient-to-br from-teal-500/10 to-cyan-500/10 border border-teal-200/40">
|
|
<Key className="w-6 h-6 text-teal-600" />
|
|
</div>
|
|
<div className="space-y-2 flex-1">
|
|
<p className="font-bold text-gray-900 text-base sm:text-lg">SMTP Server Configuration</p>
|
|
<p className="text-sm text-gray-600 leading-relaxed">
|
|
Enter your SMTP server details. These are typically provided by your email service provider.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{}
|
|
<div className="space-y-4">
|
|
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
|
<Globe className="w-4 h-4 text-gray-600" />
|
|
SMTP Host
|
|
<span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={smtpFormData.smtp_host}
|
|
onChange={(e) =>
|
|
setSmtpFormData({ ...smtpFormData, smtp_host: e.target.value })
|
|
}
|
|
placeholder={
|
|
smtpSettings?.has_host
|
|
? smtpSettings.smtp_host
|
|
: 'smtp.gmail.com'
|
|
}
|
|
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"
|
|
/>
|
|
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
<p>Server hostname (e.g., smtp.gmail.com, smtp.sendgrid.net)</p>
|
|
{smtpSettings?.has_host && (
|
|
<span className="ml-2 text-emerald-600 font-medium flex items-center gap-1">
|
|
<span className="w-2 h-2 bg-emerald-500 rounded-full"></span>
|
|
Currently configured
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="space-y-4">
|
|
<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" />
|
|
SMTP Port
|
|
<span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={smtpFormData.smtp_port}
|
|
onChange={(e) =>
|
|
setSmtpFormData({ ...smtpFormData, smtp_port: e.target.value })
|
|
}
|
|
placeholder={
|
|
smtpSettings?.smtp_port
|
|
? smtpSettings.smtp_port
|
|
: '587'
|
|
}
|
|
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"
|
|
/>
|
|
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
<p>Common ports: 587 (STARTTLS), 465 (SSL), 25 (Plain)</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{}
|
|
<div className="space-y-4">
|
|
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
|
<Mail className="w-4 h-4 text-gray-600" />
|
|
SMTP Username/Email
|
|
<span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={smtpFormData.smtp_user}
|
|
onChange={(e) =>
|
|
setSmtpFormData({ ...smtpFormData, smtp_user: e.target.value })
|
|
}
|
|
placeholder={
|
|
smtpSettings?.has_user
|
|
? smtpSettings.smtp_user
|
|
: 'your-email@gmail.com'
|
|
}
|
|
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"
|
|
/>
|
|
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
<p>Your SMTP login email or username</p>
|
|
{smtpSettings?.has_user && (
|
|
<span className="ml-2 text-emerald-600 font-medium flex items-center gap-1">
|
|
<span className="w-2 h-2 bg-emerald-500 rounded-full"></span>
|
|
Currently configured
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="space-y-4">
|
|
<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" />
|
|
SMTP Password
|
|
<span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showSmtpPassword ? 'text' : 'password'}
|
|
value={smtpFormData.smtp_password}
|
|
onChange={(e) =>
|
|
setSmtpFormData({ ...smtpFormData, smtp_password: e.target.value })
|
|
}
|
|
placeholder={
|
|
smtpSettings?.has_password
|
|
? `Current: ${smtpSettings.smtp_password_masked || '****'}`
|
|
: 'Enter password'
|
|
}
|
|
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={() => setShowSmtpPassword(!showSmtpPassword)}
|
|
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 transition-colors p-1"
|
|
>
|
|
{showSmtpPassword ? (
|
|
<EyeOff className="w-5 h-5" />
|
|
) : (
|
|
<Eye className="w-5 h-5" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
<p>Your SMTP password or app password</p>
|
|
{smtpSettings?.has_password && (
|
|
<span className="ml-2 text-emerald-600 font-medium flex items-center gap-1">
|
|
<span className="w-2 h-2 bg-emerald-500 rounded-full"></span>
|
|
Currently configured
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{}
|
|
<div className="space-y-4">
|
|
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
|
<Mail className="w-4 h-4 text-gray-600" />
|
|
From Email Address
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={smtpFormData.smtp_from_email}
|
|
onChange={(e) =>
|
|
setSmtpFormData({ ...smtpFormData, smtp_from_email: e.target.value })
|
|
}
|
|
placeholder={
|
|
smtpSettings?.smtp_from_email
|
|
? smtpSettings.smtp_from_email
|
|
: 'noreply@example.com'
|
|
}
|
|
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 sender email address (can be same as SMTP user)
|
|
</p>
|
|
</div>
|
|
|
|
{}
|
|
<div className="space-y-4">
|
|
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
|
<Globe className="w-4 h-4 text-gray-600" />
|
|
From Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={smtpFormData.smtp_from_name}
|
|
onChange={(e) =>
|
|
setSmtpFormData({ ...smtpFormData, smtp_from_name: e.target.value })
|
|
}
|
|
placeholder={
|
|
smtpSettings?.smtp_from_name
|
|
? smtpSettings.smtp_from_name
|
|
: 'Hotel Booking'
|
|
}
|
|
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">
|
|
Display name for outgoing emails
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="space-y-4">
|
|
<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" />
|
|
Use TLS/SSL
|
|
</label>
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => setSmtpFormData({ ...smtpFormData, smtp_use_tls: true })}
|
|
className={`px-6 py-3 rounded-xl font-medium transition-colors ${
|
|
smtpFormData.smtp_use_tls
|
|
? 'bg-teal-500 text-white shadow-lg'
|
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
|
}`}
|
|
>
|
|
Enabled (Port 465 - SSL)
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setSmtpFormData({ ...smtpFormData, smtp_use_tls: false })}
|
|
className={`px-6 py-3 rounded-xl font-medium transition-colors ${
|
|
!smtpFormData.smtp_use_tls
|
|
? 'bg-teal-500 text-white shadow-lg'
|
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
|
}`}
|
|
>
|
|
Disabled (Port 587 - STARTTLS)
|
|
</button>
|
|
</div>
|
|
<p className="text-xs text-gray-500">
|
|
Enable for port 465 (SSL/TLS), disable for port 587 (STARTTLS)
|
|
</p>
|
|
</div>
|
|
|
|
{}
|
|
<div className="relative mt-8 p-6 bg-gradient-to-br from-yellow-50 to-amber-50/50 border border-yellow-200/60 rounded-xl overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-yellow-400/20 to-transparent rounded-bl-full"></div>
|
|
<div className="relative space-y-3">
|
|
<p className="text-sm font-bold text-yellow-900">Common SMTP Settings</p>
|
|
<div className="space-y-2 text-xs text-yellow-800">
|
|
<p><strong>Gmail:</strong> smtp.gmail.com:587 (STARTTLS) or 465 (SSL) - Requires App Password</p>
|
|
<p><strong>SendGrid:</strong> smtp.sendgrid.net:587 - Use API key as password</p>
|
|
<p><strong>Mailgun:</strong> smtp.mailgun.org:587 or 465 - Use SMTP credentials from dashboard</p>
|
|
<p><strong>AWS SES:</strong> email-smtp.[region].amazonaws.com:587 or 465 - Use SMTP credentials</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{}
|
|
{activeTab === 'company' && (
|
|
<div className="space-y-8">
|
|
{}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2.5 rounded-xl bg-gradient-to-br from-purple-500/10 to-pink-500/10 border border-purple-200/40">
|
|
<Building2 className="w-6 h-6 text-purple-600" />
|
|
</div>
|
|
<h2 className="text-xl sm:text-2xl md:text-2xl font-extrabold text-gray-900">Company Information</h2>
|
|
</div>
|
|
<p className="text-gray-600 text-base max-w-2xl leading-relaxed">
|
|
Manage your company branding, logo, favicon, and contact information. These will be displayed across the platform and in email notifications.
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={handleSaveCompany}
|
|
disabled={saving}
|
|
className="group relative px-8 py-4 bg-gradient-to-r from-amber-500 via-amber-500 to-amber-600 text-white font-semibold rounded-xl shadow-xl shadow-amber-500/30 hover:shadow-2xl hover:shadow-amber-500/40 transition-all duration-300 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 overflow-hidden"
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div>
|
|
<div className="relative flex items-center gap-3">
|
|
<Save className={`w-5 h-5 ${saving ? 'animate-spin' : ''}`} />
|
|
{saving ? 'Saving...' : 'Save Changes'}
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-start gap-4 mb-6">
|
|
<div className="p-3 rounded-xl bg-gradient-to-br from-purple-500/10 to-pink-500/10 border border-purple-200/40">
|
|
<ImageIcon className="w-6 h-6 text-purple-600" />
|
|
</div>
|
|
<div className="space-y-2 flex-1">
|
|
<h3 className="text-base sm:text-lg md:text-lg font-bold text-gray-900">Company Logo</h3>
|
|
<p className="text-sm text-gray-600 leading-relaxed">
|
|
Upload your company logo. This will be displayed in the header, emails, and across the platform. (Max 2MB)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{logoPreview && (
|
|
<div className="relative w-full h-32 bg-gray-50 rounded-xl border-2 border-dashed border-gray-200 overflow-hidden">
|
|
<img
|
|
src={logoPreview}
|
|
alt="Logo preview"
|
|
className="w-full h-full object-contain p-4"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-gray-300 rounded-xl cursor-pointer bg-gray-50 hover:bg-gray-100 transition-colors">
|
|
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
|
<Upload className="w-8 h-8 mb-2 text-gray-400" />
|
|
<p className="mb-2 text-sm text-gray-500">
|
|
<span className="font-semibold">Click to upload</span> or drag and drop
|
|
</p>
|
|
<p className="text-xs text-gray-500">PNG, JPG, GIF up to 2MB</p>
|
|
</div>
|
|
<input
|
|
type="file"
|
|
className="hidden"
|
|
accept="image/*"
|
|
onChange={handleLogoUpload}
|
|
disabled={uploadingLogo}
|
|
/>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-start gap-4 mb-6">
|
|
<div className="p-3 rounded-xl bg-gradient-to-br from-purple-500/10 to-pink-500/10 border border-purple-200/40">
|
|
<Globe className="w-6 h-6 text-purple-600" />
|
|
</div>
|
|
<div className="space-y-2 flex-1">
|
|
<h3 className="text-base sm:text-lg md:text-lg font-bold text-gray-900">Favicon</h3>
|
|
<p className="text-sm text-gray-600 leading-relaxed">
|
|
Upload your favicon (site icon). This appears in browser tabs and bookmarks. (Max 500KB, .ico, .png, or .svg)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{faviconPreview && (
|
|
<div className="relative w-32 h-32 mx-auto bg-gray-50 rounded-xl border-2 border-dashed border-gray-200 overflow-hidden">
|
|
<img
|
|
src={faviconPreview}
|
|
alt="Favicon preview"
|
|
className="w-full h-full object-contain p-4"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-gray-300 rounded-xl cursor-pointer bg-gray-50 hover:bg-gray-100 transition-colors">
|
|
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
|
<Upload className="w-8 h-8 mb-2 text-gray-400" />
|
|
<p className="mb-2 text-sm text-gray-500">
|
|
<span className="font-semibold">Click to upload</span> or drag and drop
|
|
</p>
|
|
<p className="text-xs text-gray-500">ICO, PNG, SVG up to 500KB</p>
|
|
</div>
|
|
<input
|
|
type="file"
|
|
className="hidden"
|
|
accept=".ico,.png,.svg,image/x-icon,image/vnd.microsoft.icon"
|
|
onChange={handleFaviconUpload}
|
|
disabled={uploadingFavicon}
|
|
/>
|
|
</label>
|
|
|
|
{uploadingFavicon && (
|
|
<div className="flex items-center justify-center text-sm text-gray-600">
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-purple-600 mr-2"></div>
|
|
Uploading favicon...
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="space-y-8">
|
|
<div className="flex items-start gap-4 pb-6 border-b border-gray-200/60">
|
|
<div className="p-3 rounded-xl bg-gradient-to-br from-purple-500/10 to-pink-500/10 border border-purple-200/40">
|
|
<Building2 className="w-6 h-6 text-purple-600" />
|
|
</div>
|
|
<div className="space-y-2 flex-1">
|
|
<p className="font-bold text-gray-900 text-base sm:text-lg">Company Details</p>
|
|
<p className="text-sm text-gray-600 leading-relaxed">
|
|
Enter your company information. This will be used in email notifications, invoices, and displayed across the platform.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-6">
|
|
{}
|
|
<div className="space-y-4">
|
|
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
|
<Building2 className="w-4 h-4 text-gray-600" />
|
|
Company Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={companyFormData.company_name}
|
|
onChange={(e) =>
|
|
setCompanyFormData({ ...companyFormData, company_name: e.target.value })
|
|
}
|
|
placeholder="Luxury Hotel"
|
|
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 company or hotel name as it should appear in emails and invoices
|
|
</p>
|
|
</div>
|
|
|
|
{}
|
|
<div className="space-y-4">
|
|
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
|
<Sparkles className="w-4 h-4 text-gray-600" />
|
|
Company Tagline
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={companyFormData.company_tagline}
|
|
onChange={(e) =>
|
|
setCompanyFormData({ ...companyFormData, company_tagline: e.target.value })
|
|
}
|
|
placeholder="Excellence Redefined"
|
|
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">
|
|
A short tagline that appears below your company name in the header and footer
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{}
|
|
<div className="space-y-4">
|
|
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
|
<Globe className="w-4 h-4 text-gray-600" />
|
|
Phone Number
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={companyFormData.company_phone}
|
|
onChange={(e) =>
|
|
setCompanyFormData({ ...companyFormData, company_phone: e.target.value })
|
|
}
|
|
placeholder="+1 (555) 123-4567"
|
|
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">
|
|
Contact phone number for customers
|
|
</p>
|
|
</div>
|
|
|
|
{}
|
|
<div className="space-y-4">
|
|
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
|
<Mail className="w-4 h-4 text-gray-600" />
|
|
Email Address
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={companyFormData.company_email}
|
|
onChange={(e) =>
|
|
setCompanyFormData({ ...companyFormData, company_email: e.target.value })
|
|
}
|
|
placeholder="info@hotel.com"
|
|
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">
|
|
Contact email address for customers
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="space-y-4">
|
|
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
|
<Globe className="w-4 h-4 text-gray-600" />
|
|
Address
|
|
</label>
|
|
<textarea
|
|
value={companyFormData.company_address}
|
|
onChange={(e) =>
|
|
setCompanyFormData({ ...companyFormData, company_address: e.target.value })
|
|
}
|
|
placeholder="123 Main Street, City, State, ZIP Code"
|
|
rows={3}
|
|
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 resize-none"
|
|
/>
|
|
<p className="text-xs text-gray-500">
|
|
Physical address of your company or hotel
|
|
</p>
|
|
</div>
|
|
|
|
{}
|
|
<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 className="border-t border-gray-200 pt-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
|
<MessageCircle className="w-5 h-5 text-amber-600" />
|
|
Chat Working Hours
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-4">
|
|
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
|
<Clock className="w-4 h-4 text-gray-600" />
|
|
Start Hour (24-hour format)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
max="23"
|
|
value={companyFormData.chat_working_hours_start || 9}
|
|
onChange={(e) =>
|
|
setCompanyFormData({ ...companyFormData, chat_working_hours_start: parseInt(e.target.value) || 9 })
|
|
}
|
|
placeholder="9"
|
|
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">
|
|
Hour when chat support becomes available (0-23, e.g., 9 for 9 AM)
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
|
<Clock className="w-4 h-4 text-gray-600" />
|
|
End Hour (24-hour format)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
max="23"
|
|
value={companyFormData.chat_working_hours_end || 17}
|
|
onChange={(e) =>
|
|
setCompanyFormData({ ...companyFormData, chat_working_hours_end: parseInt(e.target.value) || 17 })
|
|
}
|
|
placeholder="17"
|
|
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">
|
|
Hour when chat support ends (0-23, e.g., 17 for 5 PM)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
<div className="relative bg-gradient-to-br from-purple-50/80 via-pink-50/60 to-purple-50/80 backdrop-blur-xl rounded-2xl shadow-xl border border-purple-200/50 p-8 overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-purple-400/20 to-transparent rounded-bl-full"></div>
|
|
<div className="relative flex gap-6">
|
|
<div className="flex-shrink-0">
|
|
<div className="p-4 rounded-xl bg-gradient-to-br from-purple-500 to-pink-600 shadow-lg border border-purple-400/50">
|
|
<Info className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-3 flex-1">
|
|
<p className="font-bold text-gray-900 text-sm sm:text-base">
|
|
How company information is used
|
|
</p>
|
|
<p className="text-gray-700 leading-relaxed">
|
|
Your company name, logo, and contact information will be automatically included in all email notifications (booking confirmations, invoices, password resets, etc.), displayed in the platform header and footer, and used in invoices and receipts. The logo and favicon will be visible to all users across the platform.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'recaptcha' && (
|
|
<div className="space-y-8">
|
|
{}
|
|
<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>
|
|
|
|
{}
|
|
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-8">
|
|
<div className="space-y-6">
|
|
{}
|
|
<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>
|
|
|
|
{}
|
|
<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>
|
|
|
|
{}
|
|
<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>
|
|
|
|
{}
|
|
<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>
|
|
|
|
{}
|
|
<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>
|
|
);
|
|
};
|
|
|
|
export default SettingsPage;
|