feat: add TypeScript definitions and utility functions for date formatting
- Created a global TypeScript declaration for the txadmin interface on the Window object. - Implemented date formatting utilities using date-fns with Spanish locale support. - Added TypeScript configuration files for app, Electron, and Node environments. - Set up Vite configuration for React application.
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import React, { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { X } from 'lucide-react';
|
||||
import { updatePlan } from '../../api/sysadmin';
|
||||
import type { Plan } from '../../api/sysadmin';
|
||||
|
||||
type EditPlanModalProps = {
|
||||
plan: Plan;
|
||||
onClose: () => void;
|
||||
onSaved: () => void;
|
||||
};
|
||||
|
||||
const numberFields: { key: keyof Plan; label: string }[] = [
|
||||
{ key: 'price', label: 'Precio mensual' },
|
||||
{ key: 'annualPrice', label: 'Precio anual' },
|
||||
{ key: 'limitOrganizations', label: 'Límite de organizaciones' },
|
||||
{ key: 'limitEmployees', label: 'Límite de empleados' },
|
||||
{ key: 'limitServices', label: 'Límite de servicios' },
|
||||
{ key: 'limitAppointments', label: 'Límite de turnos' },
|
||||
{ key: 'limitClients', label: 'Límite de clientes' },
|
||||
{ key: 'limitRepeats', label: 'Límite de repeticiones' },
|
||||
{ key: 'discount3Months', label: 'Descuento 3 meses (%)' },
|
||||
{ key: 'discount6Months', label: 'Descuento 6 meses (%)' },
|
||||
{ key: 'discount12Months', label: 'Descuento 12 meses (%)' },
|
||||
];
|
||||
|
||||
const limitFields = new Set<keyof Plan>([
|
||||
'limitOrganizations',
|
||||
'limitEmployees',
|
||||
'limitServices',
|
||||
'limitAppointments',
|
||||
'limitClients',
|
||||
'limitRepeats',
|
||||
]);
|
||||
|
||||
const booleanFields: { key: keyof Plan; label: string }[] = [
|
||||
{ key: 'active', label: 'Activo para contratación' },
|
||||
{ key: 'featured', label: 'Destacado' },
|
||||
{ key: 'mailNotifications', label: 'Notificaciones por email' },
|
||||
{ key: 'smsNotifications', label: 'Notificaciones SMS' },
|
||||
{ key: 'wapNotifications', label: 'Notificaciones WhatsApp' },
|
||||
{ key: 'payments', label: 'Pagos habilitados' },
|
||||
{ key: 'bot', label: 'Bot habilitado' },
|
||||
{ key: 'dateLimit', label: 'Tiene límite de fecha' },
|
||||
];
|
||||
|
||||
export const EditPlanModal = ({ plan, onClose, onSaved }: EditPlanModalProps) => {
|
||||
const [formData, setFormData] = useState<Plan>({
|
||||
...plan,
|
||||
features: Array.isArray(plan.features) ? plan.features : [],
|
||||
});
|
||||
const [featuresText, setFeaturesText] = useState(formData.features.join('\n'));
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const planId = plan._id || plan.id || '';
|
||||
|
||||
const setField = (key: keyof Plan, value: string | number | boolean | string[]) => {
|
||||
setFormData({ ...formData, [key]: value });
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
try {
|
||||
await updatePlan({
|
||||
planId,
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
code: formData.code,
|
||||
price: Number(formData.price) || 0,
|
||||
annualPrice: Number(formData.annualPrice) || 0,
|
||||
limitOrganizations: Number(formData.limitOrganizations) || 0,
|
||||
limitEmployees: Number(formData.limitEmployees) || 0,
|
||||
limitServices: Number(formData.limitServices) || 0,
|
||||
limitAppointments: Number(formData.limitAppointments) || 0,
|
||||
limitClients: Number(formData.limitClients) || 0,
|
||||
limitRepeats: Number(formData.limitRepeats) || 0,
|
||||
discount3Months: Number(formData.discount3Months) || 0,
|
||||
discount6Months: Number(formData.discount6Months) || 0,
|
||||
discount12Months: Number(formData.discount12Months) || 0,
|
||||
active: Boolean(formData.active),
|
||||
featured: Boolean(formData.featured),
|
||||
mailNotifications: Boolean(formData.mailNotifications),
|
||||
smsNotifications: Boolean(formData.smsNotifications),
|
||||
wapNotifications: Boolean(formData.wapNotifications),
|
||||
payments: Boolean(formData.payments),
|
||||
bot: Boolean(formData.bot),
|
||||
dateLimit: Boolean(formData.dateLimit),
|
||||
features: featuresText.split('\n').map(feature => feature.trim()).filter(Boolean),
|
||||
});
|
||||
onSaved();
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.message || err.message || 'Error guardando el plan');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 50, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(4px)' }}>
|
||||
<motion.div initial={{ scale: 0.9, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} className="glass-panel" style={{ width: '94%', maxWidth: '860px', maxHeight: '90vh', overflowY: 'auto', padding: '2rem', position: 'relative' }}>
|
||||
<button onClick={onClose} style={{ position: 'absolute', top: '1.5rem', right: '1.5rem', background: 'transparent', color: 'var(--text-muted)' }}>
|
||||
<X size={24} />
|
||||
</button>
|
||||
|
||||
<h2 style={{ fontSize: '1.5rem', marginBottom: '1.5rem' }}>Editar plan: {plan.name}</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '1rem' }}>
|
||||
<div>
|
||||
<label className="modal-label">Nombre</label>
|
||||
<input className="input-glass" value={formData.name} onChange={e => setField('name', e.target.value)} required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="modal-label">Código</label>
|
||||
<input className="input-glass" value={formData.code} onChange={e => setField('code', e.target.value)} required />
|
||||
</div>
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<label className="modal-label">Descripción</label>
|
||||
<textarea className="input-glass" rows={3} value={formData.description} onChange={e => setField('description', e.target.value)} required />
|
||||
</div>
|
||||
|
||||
<h3 className="plan-modal-section">Precios, límites y descuentos</h3>
|
||||
{numberFields.map(field => (
|
||||
<div key={field.key}>
|
||||
<label className="modal-label">{field.label}</label>
|
||||
<input className="input-glass" type="number" min={limitFields.has(field.key) ? undefined : '0'} step="0.01" value={Number(formData[field.key] || 0)} onChange={e => setField(field.key, Number(e.target.value))} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
<h3 className="plan-modal-section">Funciones y estado</h3>
|
||||
{booleanFields.map(field => (
|
||||
<label key={field.key} className="plan-toggle">
|
||||
<input type="checkbox" checked={Boolean(formData[field.key])} onChange={e => setField(field.key, e.target.checked)} />
|
||||
{field.label}
|
||||
</label>
|
||||
))}
|
||||
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<label className="modal-label">Features visibles (una por línea)</label>
|
||||
<textarea className="input-glass" rows={5} value={featuresText} onChange={e => setFeaturesText(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div style={{ gridColumn: '1 / -1', marginTop: '1rem', display: 'flex', justifyContent: 'flex-end', gap: '1rem' }}>
|
||||
<button type="button" className="btn-secondary" onClick={onClose}>Cancelar</button>
|
||||
<button type="submit" className="btn-primary" disabled={loading || !planId}>{loading ? 'Guardando...' : 'Guardar cambios'}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style dangerouslySetInnerHTML={{__html: `
|
||||
.modal-label { display: block; font-size: 0.85rem; color: var(--text-muted); margin-bottom: 0.5rem; }
|
||||
.plan-modal-section { grid-column: 1 / -1; margin: 1rem 0 0.25rem; color: var(--primary); font-size: 1.05rem; }
|
||||
.plan-toggle { display: flex; align-items: center; gap: 0.6rem; color: var(--text-main); background: rgba(255,255,255,0.04); border: 1px solid var(--glass-border); border-radius: 10px; padding: 0.75rem; }
|
||||
.plan-toggle input { width: 16px; height: 16px; }
|
||||
@media (max-width: 768px) { form { grid-template-columns: 1fr !important; } }
|
||||
`}} />
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user