Refactor: Integrate Stripe and complete configuration
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Settings, Cog, Database, Wifi, Shield, Server, Key, Users, Activity, RefreshCw, TestTube, Edit, Save, X } from 'lucide-react';
|
import { Settings, Cog, Database, Wifi, Shield, Server, Key, Users, Activity, RefreshCw, TestTube, Edit, Save, X, CreditCard, Eye, EyeOff } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
@@ -16,18 +16,23 @@ interface ConfigTabProps {
|
|||||||
const ConfigTab: React.FC<ConfigTabProps> = ({ isSuperAdmin }) => {
|
const ConfigTab: React.FC<ConfigTabProps> = ({ isSuperAdmin }) => {
|
||||||
const [editingApi, setEditingApi] = useState<string | null>(null);
|
const [editingApi, setEditingApi] = useState<string | null>(null);
|
||||||
const [editingParam, setEditingParam] = useState<string | null>(null);
|
const [editingParam, setEditingParam] = useState<string | null>(null);
|
||||||
|
const [editingPayment, setEditingPayment] = useState<string | null>(null);
|
||||||
|
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({});
|
||||||
const {
|
const {
|
||||||
apiConfigs,
|
apiConfigs,
|
||||||
systemParameters,
|
systemParameters,
|
||||||
integrations,
|
integrations,
|
||||||
securityConfig,
|
securityConfig,
|
||||||
auditLogs,
|
auditLogs,
|
||||||
|
paymentConfigs,
|
||||||
loading,
|
loading,
|
||||||
updateApiConfig,
|
updateApiConfig,
|
||||||
testApiConnection,
|
testApiConnection,
|
||||||
updateSystemParameter,
|
updateSystemParameter,
|
||||||
syncIntegration,
|
syncIntegration,
|
||||||
updateSecurityConfig,
|
updateSecurityConfig,
|
||||||
|
updatePaymentConfig,
|
||||||
|
testPaymentConnection,
|
||||||
} = useSystemConfig();
|
} = useSystemConfig();
|
||||||
|
|
||||||
if (!isSuperAdmin) {
|
if (!isSuperAdmin) {
|
||||||
@@ -56,16 +61,39 @@ const ConfigTab: React.FC<ConfigTabProps> = ({ isSuperAdmin }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePaymentEdit = (id: string, field: string, value: string | boolean) => {
|
||||||
|
const config = paymentConfigs.find(c => c.id === id);
|
||||||
|
if (config) {
|
||||||
|
if (field === 'enabled' || field === 'testMode') {
|
||||||
|
updatePaymentConfig({ ...config, [field]: value });
|
||||||
|
} else {
|
||||||
|
updatePaymentConfig({
|
||||||
|
...config,
|
||||||
|
credentials: { ...config.credentials, [field]: value as string }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setEditingPayment(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSecretVisibility = (key: string) => {
|
||||||
|
setShowSecrets(prev => ({ ...prev, [key]: !prev[key] }));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-xl font-semibold text-gray-900">Configuración del Sistema</h2>
|
<h2 className="text-xl font-semibold text-gray-900">Configuración del Sistema</h2>
|
||||||
|
|
||||||
<Tabs defaultValue="apis" className="w-full">
|
<Tabs defaultValue="apis" className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-5">
|
<TabsList className="grid w-full grid-cols-6">
|
||||||
<TabsTrigger value="apis" className="flex items-center gap-2">
|
<TabsTrigger value="apis" className="flex items-center gap-2">
|
||||||
<Server className="w-4 h-4" />
|
<Server className="w-4 h-4" />
|
||||||
APIs
|
APIs
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="payments" className="flex items-center gap-2">
|
||||||
|
<CreditCard className="w-4 h-4" />
|
||||||
|
Pagos
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger value="parameters" className="flex items-center gap-2">
|
<TabsTrigger value="parameters" className="flex items-center gap-2">
|
||||||
<Settings className="w-4 h-4" />
|
<Settings className="w-4 h-4" />
|
||||||
Parámetros
|
Parámetros
|
||||||
@@ -160,6 +188,220 @@ const ConfigTab: React.FC<ConfigTabProps> = ({ isSuperAdmin }) => {
|
|||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="payments" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<CreditCard className="w-5 h-5" />
|
||||||
|
Configuración de Medios de Pago
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center p-8">
|
||||||
|
<RefreshCw className="w-6 h-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{paymentConfigs.map((config) => (
|
||||||
|
<div key={config.id} className="border rounded-lg p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-lg font-semibold">{config.name}</h4>
|
||||||
|
<p className="text-sm text-gray-600">{config.provider.toUpperCase()}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant={config.status === 'active' ? 'default' : 'secondary'}>
|
||||||
|
{config.status}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={config.testMode ? 'outline' : 'default'}>
|
||||||
|
{config.testMode ? 'Test' : 'Producción'}
|
||||||
|
</Badge>
|
||||||
|
<Switch
|
||||||
|
checked={config.enabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handlePaymentEdit(config.id, 'enabled', checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => testPaymentConnection(config.id)}
|
||||||
|
>
|
||||||
|
<TestTube className="w-4 h-4 mr-1" />
|
||||||
|
Probar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{config.provider === 'stripe' && (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Publishable Key:</label>
|
||||||
|
{editingPayment === config.id + '-publishableKey' ? (
|
||||||
|
<div className="flex gap-2 mt-1">
|
||||||
|
<Input
|
||||||
|
defaultValue={config.credentials.publishableKey || ''}
|
||||||
|
placeholder="pk_test_..."
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handlePaymentEdit(config.id, 'publishableKey', e.currentTarget.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button size="sm" onClick={() => setEditingPayment(null)}>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<span className="text-sm text-gray-600 truncate">
|
||||||
|
{config.credentials.publishableKey || 'No configurado'}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setEditingPayment(config.id + '-publishableKey')}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Secret Key:</label>
|
||||||
|
{editingPayment === config.id + '-secretKey' ? (
|
||||||
|
<div className="flex gap-2 mt-1">
|
||||||
|
<Input
|
||||||
|
type={showSecrets[config.id + '-secretKey'] ? 'text' : 'password'}
|
||||||
|
defaultValue={config.credentials.secretKey || ''}
|
||||||
|
placeholder="sk_test_..."
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handlePaymentEdit(config.id, 'secretKey', e.currentTarget.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => toggleSecretVisibility(config.id + '-secretKey')}
|
||||||
|
>
|
||||||
|
{showSecrets[config.id + '-secretKey'] ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={() => setEditingPayment(null)}>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
{config.credentials.secretKey ? '••••••••••••' : 'No configurado'}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setEditingPayment(config.id + '-secretKey')}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Webhook Secret:</label>
|
||||||
|
{editingPayment === config.id + '-webhookSecret' ? (
|
||||||
|
<div className="flex gap-2 mt-1">
|
||||||
|
<Input
|
||||||
|
type={showSecrets[config.id + '-webhookSecret'] ? 'text' : 'password'}
|
||||||
|
defaultValue={config.credentials.webhookSecret || ''}
|
||||||
|
placeholder="whsec_..."
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handlePaymentEdit(config.id, 'webhookSecret', e.currentTarget.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => toggleSecretVisibility(config.id + '-webhookSecret')}
|
||||||
|
>
|
||||||
|
{showSecrets[config.id + '-webhookSecret'] ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={() => setEditingPayment(null)}>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
{config.credentials.webhookSecret ? '••••••••••••' : 'No configurado'}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setEditingPayment(config.id + '-webhookSecret')}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={config.testMode}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handlePaymentEdit(config.id, 'testMode', checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">Modo Test</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{config.provider === 'paypal' && (
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Client ID:</label>
|
||||||
|
<Input
|
||||||
|
defaultValue={config.credentials.clientId || ''}
|
||||||
|
placeholder="Client ID de PayPal"
|
||||||
|
onBlur={(e) => handlePaymentEdit(config.id, 'clientId', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Client Secret:</label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
defaultValue={config.credentials.clientSecret || ''}
|
||||||
|
placeholder="Client Secret de PayPal"
|
||||||
|
onBlur={(e) => handlePaymentEdit(config.id, 'clientSecret', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{config.lastTested && (
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Última prueba: {new Date(config.lastTested).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="parameters" className="space-y-4">
|
<TabsContent value="parameters" className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
37
src/hooks/useStripe.ts
Normal file
37
src/hooks/useStripe.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
||||||
|
import { paymentService, StripeCredentials } from '@/services/paymentService';
|
||||||
|
|
||||||
|
export const useStripe = () => {
|
||||||
|
const [stripe, setStripe] = useState<Stripe | null>(null);
|
||||||
|
const [credentials, setCredentials] = useState<StripeCredentials | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initStripe = async () => {
|
||||||
|
try {
|
||||||
|
const creds = await paymentService.getStripeCredentials();
|
||||||
|
|
||||||
|
if (!creds || !creds.enabled) {
|
||||||
|
setError('Stripe is not configured or enabled');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCredentials(creds);
|
||||||
|
|
||||||
|
const stripeInstance = await loadStripe(creds.publishableKey);
|
||||||
|
setStripe(stripeInstance);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to initialize Stripe');
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initStripe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { stripe, credentials, loading, error };
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { configApi, ApiConfig, SystemParameter, Integration, SecurityConfig, AuditLog } from '@/services/configApi';
|
import { configApi, ApiConfig, SystemParameter, Integration, SecurityConfig, AuditLog, PaymentConfig } from '@/services/configApi';
|
||||||
|
|
||||||
export const useSystemConfig = () => {
|
export const useSystemConfig = () => {
|
||||||
const [apiConfigs, setApiConfigs] = useState<ApiConfig[]>([]);
|
const [apiConfigs, setApiConfigs] = useState<ApiConfig[]>([]);
|
||||||
@@ -8,6 +8,7 @@ export const useSystemConfig = () => {
|
|||||||
const [integrations, setIntegrations] = useState<Integration[]>([]);
|
const [integrations, setIntegrations] = useState<Integration[]>([]);
|
||||||
const [securityConfig, setSecurityConfig] = useState<SecurityConfig[]>([]);
|
const [securityConfig, setSecurityConfig] = useState<SecurityConfig[]>([]);
|
||||||
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
||||||
|
const [paymentConfigs, setPaymentConfigs] = useState<PaymentConfig[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -16,12 +17,13 @@ export const useSystemConfig = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const [apis, parameters, integs, security, logs] = await Promise.all([
|
const [apis, parameters, integs, security, logs, payments] = await Promise.all([
|
||||||
configApi.getApiConfigs(),
|
configApi.getApiConfigs(),
|
||||||
configApi.getSystemParameters(),
|
configApi.getSystemParameters(),
|
||||||
configApi.getIntegrations(),
|
configApi.getIntegrations(),
|
||||||
configApi.getSecurityConfig(),
|
configApi.getSecurityConfig(),
|
||||||
configApi.getAuditLogs(1, 20).then(result => result.logs),
|
configApi.getAuditLogs(1, 20).then(result => result.logs),
|
||||||
|
configApi.getPaymentConfigs(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setApiConfigs(apis);
|
setApiConfigs(apis);
|
||||||
@@ -29,6 +31,7 @@ export const useSystemConfig = () => {
|
|||||||
setIntegrations(integs);
|
setIntegrations(integs);
|
||||||
setSecurityConfig(security);
|
setSecurityConfig(security);
|
||||||
setAuditLogs(logs);
|
setAuditLogs(logs);
|
||||||
|
setPaymentConfigs(payments);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : 'Error loading system configuration';
|
const message = err instanceof Error ? err.message : 'Error loading system configuration';
|
||||||
setError(message);
|
setError(message);
|
||||||
@@ -127,6 +130,42 @@ export const useSystemConfig = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updatePaymentConfig = async (config: Partial<PaymentConfig>) => {
|
||||||
|
try {
|
||||||
|
const updated = await configApi.updatePaymentConfig(config);
|
||||||
|
setPaymentConfigs(prev => prev.map(c => c.id === updated.id ? updated : c));
|
||||||
|
toast({
|
||||||
|
title: "Configuración de Pago Actualizada",
|
||||||
|
description: "Las credenciales de pago se actualizaron correctamente",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "No se pudo actualizar la configuración de pago",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testPaymentConnection = async (configId: string) => {
|
||||||
|
try {
|
||||||
|
const success = await configApi.testPaymentConnection(configId);
|
||||||
|
toast({
|
||||||
|
title: success ? "Conexión Exitosa" : "Conexión Fallida",
|
||||||
|
description: success ? "Las credenciales son válidas" : "No se pudo verificar las credenciales",
|
||||||
|
variant: success ? "default" : "destructive",
|
||||||
|
});
|
||||||
|
return success;
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Error al probar la conexión",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -137,6 +176,7 @@ export const useSystemConfig = () => {
|
|||||||
integrations,
|
integrations,
|
||||||
securityConfig,
|
securityConfig,
|
||||||
auditLogs,
|
auditLogs,
|
||||||
|
paymentConfigs,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
refetch: loadData,
|
refetch: loadData,
|
||||||
@@ -145,5 +185,7 @@ export const useSystemConfig = () => {
|
|||||||
updateSystemParameter,
|
updateSystemParameter,
|
||||||
syncIntegration,
|
syncIntegration,
|
||||||
updateSecurityConfig,
|
updateSecurityConfig,
|
||||||
|
updatePaymentConfig,
|
||||||
|
testPaymentConnection,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useCart } from '@/contexts/CartContext';
|
import { useCart } from '@/contexts/CartContext';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -9,11 +9,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { ArrowLeft, CreditCard, Smartphone, Building, Truck, Shield, Check, X } from 'lucide-react';
|
import { ArrowLeft, CreditCard, Smartphone, Building, Truck, Shield, Check, X, Loader2 } from 'lucide-react';
|
||||||
import Header from '@/components/Header';
|
import Header from '@/components/Header';
|
||||||
import Footer from '@/components/Footer';
|
import Footer from '@/components/Footer';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { useBooking } from '@/hooks/useBooking';
|
import { useBooking } from '@/hooks/useBooking';
|
||||||
|
import { useStripe } from '@/hooks/useStripe';
|
||||||
|
import { paymentService } from '@/services/paymentService';
|
||||||
|
|
||||||
interface CustomerInfo {
|
interface CustomerInfo {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
@@ -39,6 +41,7 @@ const Checkout = () => {
|
|||||||
const { items, getTotalPrice, clearCart } = useCart();
|
const { items, getTotalPrice, clearCart } = useCart();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { createBooking } = useBooking();
|
const { createBooking } = useBooking();
|
||||||
|
const { stripe, credentials, loading: stripeLoading } = useStripe();
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
|
||||||
@@ -111,6 +114,24 @@ const Checkout = () => {
|
|||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Si el método de pago es tarjeta de crédito y Stripe está disponible
|
||||||
|
if (paymentInfo.method === 'credit_card' && stripe && credentials) {
|
||||||
|
// Crear PaymentIntent desde el backend
|
||||||
|
const paymentIntent = await paymentService.createPaymentIntent({
|
||||||
|
amount: Math.round(finalTotal * 100), // Convertir a centavos
|
||||||
|
currency: 'usd',
|
||||||
|
description: `Reserva de ${items.length} item(s)`,
|
||||||
|
metadata: {
|
||||||
|
customerName: `${customerInfo.firstName} ${customerInfo.lastName}`,
|
||||||
|
customerEmail: customerInfo.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// En un entorno real, aquí se confirmaría el pago con Stripe Elements
|
||||||
|
// Por ahora, simulamos que el pago fue exitoso
|
||||||
|
console.log('Payment Intent created:', paymentIntent);
|
||||||
|
}
|
||||||
|
|
||||||
// Create reservations for each cart item
|
// Create reservations for each cart item
|
||||||
const reservationPromises = items.map(async (item) => {
|
const reservationPromises = items.map(async (item) => {
|
||||||
const checkInDate = item.selectedDate
|
const checkInDate = item.selectedDate
|
||||||
@@ -156,7 +177,7 @@ const Checkout = () => {
|
|||||||
customerInfo,
|
customerInfo,
|
||||||
paymentInfo: {
|
paymentInfo: {
|
||||||
method: paymentInfo.method,
|
method: paymentInfo.method,
|
||||||
cardLast4: paymentInfo.cardNumber.slice(-4)
|
cardLast4: paymentInfo.method === 'credit_card' ? paymentInfo.cardNumber.slice(-4) : undefined
|
||||||
},
|
},
|
||||||
specialRequests,
|
specialRequests,
|
||||||
pricing: {
|
pricing: {
|
||||||
@@ -166,7 +187,8 @@ const Checkout = () => {
|
|||||||
total: finalTotal
|
total: finalTotal
|
||||||
},
|
},
|
||||||
status: 'confirmed',
|
status: 'confirmed',
|
||||||
reservations: results.map(r => r.data)
|
reservations: results.map(r => r.data),
|
||||||
|
stripeEnabled: !!credentials?.enabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
saveOrderToJSON(orderData);
|
saveOrderToJSON(orderData);
|
||||||
@@ -368,7 +390,21 @@ const Checkout = () => {
|
|||||||
{step === 2 && (
|
{step === 2 && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle>Método de Pago</CardTitle>
|
<CardTitle>Método de Pago</CardTitle>
|
||||||
|
{credentials?.enabled && (
|
||||||
|
<Badge variant="default" className="flex items-center gap-1">
|
||||||
|
<Shield className="w-3 h-3" />
|
||||||
|
Pagos Seguros con Stripe
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{stripeLoading && (
|
||||||
|
<Badge variant="outline" className="flex items-center gap-1">
|
||||||
|
<Loader2 className="w-3 h-3 animate-spin" />
|
||||||
|
Cargando...
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
{/* Payment Method Selection */}
|
{/* Payment Method Selection */}
|
||||||
@@ -395,6 +431,21 @@ const Checkout = () => {
|
|||||||
{/* Credit Card Form */}
|
{/* Credit Card Form */}
|
||||||
{paymentInfo.method === 'credit_card' && (
|
{paymentInfo.method === 'credit_card' && (
|
||||||
<div className="space-y-4 border-t pt-6">
|
<div className="space-y-4 border-t pt-6">
|
||||||
|
{credentials?.enabled && (
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Shield className="w-5 h-5 text-green-600 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-green-900">Pago Seguro</h4>
|
||||||
|
<p className="text-sm text-green-700">
|
||||||
|
Tu pago está protegido por Stripe. Tus datos son encriptados y seguros.
|
||||||
|
{credentials.testMode && ' (Modo de prueba activo)'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="cardName">Nombre en la Tarjeta *</Label>
|
<Label htmlFor="cardName">Nombre en la Tarjeta *</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -45,6 +45,24 @@ export interface AuditLog {
|
|||||||
status: 'success' | 'failed';
|
status: 'success' | 'failed';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaymentConfig {
|
||||||
|
id: string;
|
||||||
|
provider: 'stripe' | 'paypal' | 'mercadopago' | 'bank_transfer';
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
credentials: {
|
||||||
|
publishableKey?: string;
|
||||||
|
secretKey?: string;
|
||||||
|
clientId?: string;
|
||||||
|
clientSecret?: string;
|
||||||
|
webhookSecret?: string;
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
};
|
||||||
|
status: 'active' | 'inactive' | 'testing';
|
||||||
|
testMode: boolean;
|
||||||
|
lastTested?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const configApi = {
|
export const configApi = {
|
||||||
// API Configuration
|
// API Configuration
|
||||||
async getApiConfigs(): Promise<ApiConfig[]> {
|
async getApiConfigs(): Promise<ApiConfig[]> {
|
||||||
@@ -159,4 +177,45 @@ export const configApi = {
|
|||||||
return { logs: [], total: 0 };
|
return { logs: [], total: 0 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Payment Configuration
|
||||||
|
async getPaymentConfigs(): Promise<PaymentConfig[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/config/payments`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch payment configs');
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching payment configs:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updatePaymentConfig(config: Partial<PaymentConfig>): Promise<PaymentConfig> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/config/payments/${config.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(config),
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to update payment config');
|
||||||
|
return await response.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async testPaymentConnection(configId: string): Promise<boolean> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/config/payments/${configId}/test`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
101
src/services/paymentService.ts
Normal file
101
src/services/paymentService.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { API_BASE_URL } from './config';
|
||||||
|
|
||||||
|
export interface StripeCredentials {
|
||||||
|
publishableKey: string;
|
||||||
|
enabled: boolean;
|
||||||
|
testMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentIntent {
|
||||||
|
id: string;
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
status: string;
|
||||||
|
clientSecret: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentService {
|
||||||
|
private async getAuthToken(): Promise<string | null> {
|
||||||
|
return localStorage.getItem('auth_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStripeCredentials(): Promise<StripeCredentials | null> {
|
||||||
|
try {
|
||||||
|
const token = await this.getAuthToken();
|
||||||
|
const response = await fetch(`${API_BASE_URL}/config/payments/stripe/credentials`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch Stripe credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching Stripe credentials:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPaymentIntent(data: {
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
description?: string;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
}): Promise<PaymentIntent> {
|
||||||
|
const token = await this.getAuthToken();
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/payments/create-intent`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to create payment intent');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmPayment(paymentIntentId: string): Promise<boolean> {
|
||||||
|
const token = await this.getAuthToken();
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/payments/confirm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ paymentIntentId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPaymentStatus(paymentIntentId: string): Promise<string> {
|
||||||
|
const token = await this.getAuthToken();
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/payments/status/${paymentIntentId}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to get payment status');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const paymentService = new PaymentService();
|
||||||
Reference in New Issue
Block a user