feat: Add full API integration to Quality Template, Equipment, and Team Member wizards
- QualityTemplateWizard: Fixed onComplete bug and added API save via qualityTemplateService - EquipmentWizard: Added API save via equipmentService with loading states and error handling - TeamMemberWizard: Added API save via authService for user registration with permissions All three wizards now: - Use useTenant hook to get tenant ID - Call actual backend APIs instead of console.log - Include loading states during API calls - Show error messages if API calls fail - Properly handle success/failure scenarios
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
||||||
import { Wrench, CheckCircle2 } from 'lucide-react';
|
import { Wrench, CheckCircle2, Loader2 } from 'lucide-react';
|
||||||
|
import { useTenant } from '../../../../stores/tenant.store';
|
||||||
|
import { equipmentService } from '../../../../api/services/equipment';
|
||||||
|
|
||||||
interface WizardDataProps extends WizardStepProps {
|
interface WizardDataProps extends WizardStepProps {
|
||||||
data: Record<string, any>;
|
data: Record<string, any>;
|
||||||
@@ -8,6 +10,7 @@ interface WizardDataProps extends WizardStepProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
||||||
|
const { currentTenant } = useTenant();
|
||||||
const [equipmentData, setEquipmentData] = useState({
|
const [equipmentData, setEquipmentData] = useState({
|
||||||
type: data.type || 'oven',
|
type: data.type || 'oven',
|
||||||
brand: data.brand || '',
|
brand: data.brand || '',
|
||||||
@@ -16,6 +19,44 @@ const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
|||||||
purchaseDate: data.purchaseDate || '',
|
purchaseDate: data.purchaseDate || '',
|
||||||
status: data.status || 'active',
|
status: data.status || 'active',
|
||||||
});
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!currentTenant?.id) {
|
||||||
|
setError('No se pudo obtener información del tenant');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const equipmentCreateData: any = {
|
||||||
|
name: `${equipmentData.type} - ${equipmentData.brand || 'Sin marca'}`,
|
||||||
|
type: equipmentData.type,
|
||||||
|
model: equipmentData.brand,
|
||||||
|
serialNumber: equipmentData.model,
|
||||||
|
location: equipmentData.location,
|
||||||
|
status: equipmentData.status,
|
||||||
|
installDate: equipmentData.purchaseDate || new Date().toISOString().split('T')[0],
|
||||||
|
lastMaintenance: new Date().toISOString().split('T')[0],
|
||||||
|
nextMaintenance: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||||
|
maintenanceInterval: 30,
|
||||||
|
is_active: true
|
||||||
|
};
|
||||||
|
|
||||||
|
await equipmentService.createEquipment(currentTenant.id, equipmentCreateData);
|
||||||
|
|
||||||
|
onDataChange({ ...data, ...equipmentData });
|
||||||
|
onComplete();
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Error creating equipment:', err);
|
||||||
|
setError(err.response?.data?.detail || 'Error al crear el equipo');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -23,10 +64,21 @@ const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
|||||||
<Wrench className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
<Wrench className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Equipo de Panadería</h3>
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Equipo de Panadería</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Equipo *</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Equipo *</label>
|
||||||
<select value={equipmentData.type} onChange={(e) => setEquipmentData({ ...equipmentData, type: e.target.value })} className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]">
|
<select
|
||||||
|
value={equipmentData.type}
|
||||||
|
onChange={(e) => setEquipmentData({ ...equipmentData, type: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
>
|
||||||
<option value="oven">Horno</option>
|
<option value="oven">Horno</option>
|
||||||
<option value="mixer">Amasadora</option>
|
<option value="mixer">Amasadora</option>
|
||||||
<option value="proofer">Fermentadora</option>
|
<option value="proofer">Fermentadora</option>
|
||||||
@@ -36,19 +88,53 @@ const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Marca/Modelo</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Marca/Modelo</label>
|
||||||
<input type="text" value={equipmentData.brand} onChange={(e) => setEquipmentData({ ...equipmentData, brand: e.target.value })} placeholder="Ej: Rational SCC 101" className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]" />
|
<input
|
||||||
|
type="text"
|
||||||
|
value={equipmentData.brand}
|
||||||
|
onChange={(e) => setEquipmentData({ ...equipmentData, brand: e.target.value })}
|
||||||
|
placeholder="Ej: Rational SCC 101"
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Ubicación</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Ubicación</label>
|
||||||
<input type="text" value={equipmentData.location} onChange={(e) => setEquipmentData({ ...equipmentData, location: e.target.value })} placeholder="Ej: Cocina principal" className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]" />
|
<input
|
||||||
|
type="text"
|
||||||
|
value={equipmentData.location}
|
||||||
|
onChange={(e) => setEquipmentData({ ...equipmentData, location: e.target.value })}
|
||||||
|
placeholder="Ej: Cocina principal"
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Fecha de Compra</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Fecha de Compra</label>
|
||||||
<input type="date" value={equipmentData.purchaseDate} onChange={(e) => setEquipmentData({ ...equipmentData, purchaseDate: e.target.value })} className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]" />
|
<input
|
||||||
|
type="date"
|
||||||
|
value={equipmentData.purchaseDate}
|
||||||
|
onChange={(e) => setEquipmentData({ ...equipmentData, purchaseDate: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
||||||
<button onClick={() => { onDataChange({ ...data, ...equipmentData }); console.log('Saving equipment:', { ...data, ...equipmentData }); onComplete(); }} className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2"><CheckCircle2 className="w-5 h-5" />Agregar Equipo</button>
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
|
Guardando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle2 className="w-5 h-5" />
|
||||||
|
Agregar Equipo
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,76 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
||||||
import { ClipboardCheck, ListChecks, CheckCircle2 } from 'lucide-react';
|
import { ClipboardCheck, CheckCircle2, Loader2 } from 'lucide-react';
|
||||||
|
import { useTenant } from '../../../../stores/tenant.store';
|
||||||
|
import { qualityTemplateService } from '../../../../api/services/qualityTemplates';
|
||||||
|
import { QualityCheckTemplateCreate } from '../../../../api/types/qualityTemplates';
|
||||||
|
|
||||||
interface WizardDataProps extends WizardStepProps {
|
interface WizardDataProps extends WizardStepProps {
|
||||||
data: Record<string, any>;
|
data: Record<string, any>;
|
||||||
onDataChange: (data: Record<string, any>) => void;
|
onDataChange: (data: Record<string, any>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TemplateInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext }) => {
|
const TemplateInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
||||||
|
const { currentTenant } = useTenant();
|
||||||
const [templateData, setTemplateData] = useState({
|
const [templateData, setTemplateData] = useState({
|
||||||
name: data.name || '',
|
name: data.name || '',
|
||||||
scope: data.scope || 'product',
|
scope: data.scope || 'product',
|
||||||
frequency: data.frequency || 'batch',
|
frequency: data.frequency || 'batch',
|
||||||
});
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!currentTenant?.id) {
|
||||||
|
setError('No se pudo obtener información del tenant');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const scopeMapping: Record<string, string> = {
|
||||||
|
product: 'product_quality',
|
||||||
|
process: 'process_hygiene',
|
||||||
|
equipment: 'equipment',
|
||||||
|
safety: 'safety'
|
||||||
|
};
|
||||||
|
|
||||||
|
const templateCreateData: QualityCheckTemplateCreate = {
|
||||||
|
name: templateData.name,
|
||||||
|
description: `Plantilla de ${templateData.scope} con frecuencia ${templateData.frequency}`,
|
||||||
|
check_type: scopeMapping[templateData.scope] || 'product_quality',
|
||||||
|
applicable_stages: [],
|
||||||
|
check_points: [
|
||||||
|
{
|
||||||
|
name: 'Verificación General',
|
||||||
|
description: 'Punto de verificación inicial',
|
||||||
|
expected_value: 'Conforme',
|
||||||
|
measurement_type: 'pass_fail',
|
||||||
|
is_critical: false,
|
||||||
|
weight: 1.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
scoring_method: 'weighted_average',
|
||||||
|
pass_threshold: 70.0,
|
||||||
|
weight: 5.0,
|
||||||
|
is_required: templateData.frequency === 'batch',
|
||||||
|
is_active: true,
|
||||||
|
frequency_days: templateData.frequency === 'daily' ? 1 : templateData.frequency === 'weekly' ? 7 : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
await qualityTemplateService.createTemplate(currentTenant.id, templateCreateData);
|
||||||
|
|
||||||
|
onDataChange({ ...data, ...templateData });
|
||||||
|
onComplete();
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Error creating quality template:', err);
|
||||||
|
setError(err.response?.data?.detail || 'Error al crear la plantilla de calidad');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -20,14 +78,31 @@ const TemplateInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNex
|
|||||||
<ClipboardCheck className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
<ClipboardCheck className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Plantilla de Calidad</h3>
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Plantilla de Calidad</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Nombre *</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Nombre *</label>
|
||||||
<input type="text" value={templateData.name} onChange={(e) => setTemplateData({ ...templateData, name: e.target.value })} placeholder="Ej: Control de Calidad de Pan" className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]" />
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateData.name}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, name: e.target.value })}
|
||||||
|
placeholder="Ej: Control de Calidad de Pan"
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Alcance *</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Alcance *</label>
|
||||||
<select value={templateData.scope} onChange={(e) => setTemplateData({ ...templateData, scope: e.target.value })} className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]">
|
<select
|
||||||
|
value={templateData.scope}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, scope: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
>
|
||||||
<option value="product">Calidad de Producto</option>
|
<option value="product">Calidad de Producto</option>
|
||||||
<option value="process">Higiene de Proceso</option>
|
<option value="process">Higiene de Proceso</option>
|
||||||
<option value="equipment">Equipo</option>
|
<option value="equipment">Equipo</option>
|
||||||
@@ -36,15 +111,36 @@ const TemplateInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNex
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Frecuencia *</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Frecuencia *</label>
|
||||||
<select value={templateData.frequency} onChange={(e) => setTemplateData({ ...templateData, frequency: e.target.value })} className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]">
|
<select
|
||||||
|
value={templateData.frequency}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, frequency: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
>
|
||||||
<option value="batch">Cada Lote</option>
|
<option value="batch">Cada Lote</option>
|
||||||
<option value="daily">Diario</option>
|
<option value="daily">Diario</option>
|
||||||
<option value="weekly">Semanal</option>
|
<option value="weekly">Semanal</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
||||||
<button onClick={() => { onDataChange({ ...data, ...templateData }); onComplete(); }} disabled={!templateData.name} className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2"><CheckCircle2 className="w-5 h-5" />Crear Plantilla</button>
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!templateData.name || loading}
|
||||||
|
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
|
Guardando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle2 className="w-5 h-5" />
|
||||||
|
Crear Plantilla
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
||||||
import { UserPlus, Shield, CheckCircle2, Mail, Phone } from 'lucide-react';
|
import { UserPlus, Shield, CheckCircle2, Mail, Phone, Loader2 } from 'lucide-react';
|
||||||
|
import { useTenant } from '../../../../stores/tenant.store';
|
||||||
|
import { authService } from '../../../../api/services/auth';
|
||||||
|
|
||||||
interface WizardDataProps extends WizardStepProps {
|
interface WizardDataProps extends WizardStepProps {
|
||||||
data: Record<string, any>;
|
data: Record<string, any>;
|
||||||
@@ -25,19 +27,47 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Nombre Completo *</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Nombre Completo *</label>
|
||||||
<input type="text" value={memberData.fullName} onChange={(e) => setMemberData({ ...memberData, fullName: e.target.value })} placeholder="Ej: Juan García" className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]" />
|
<input
|
||||||
|
type="text"
|
||||||
|
value={memberData.fullName}
|
||||||
|
onChange={(e) => setMemberData({ ...memberData, fullName: e.target.value })}
|
||||||
|
placeholder="Ej: Juan García"
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2"><Mail className="w-3.5 h-3.5 inline mr-1" />Email *</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
<input type="email" value={memberData.email} onChange={(e) => setMemberData({ ...memberData, email: e.target.value })} placeholder="juan@panaderia.com" className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]" />
|
<Mail className="w-3.5 h-3.5 inline mr-1" />
|
||||||
|
Email *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={memberData.email}
|
||||||
|
onChange={(e) => setMemberData({ ...memberData, email: e.target.value })}
|
||||||
|
placeholder="juan@panaderia.com"
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2"><Phone className="w-3.5 h-3.5 inline mr-1" />Teléfono</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
<input type="tel" value={memberData.phone} onChange={(e) => setMemberData({ ...memberData, phone: e.target.value })} placeholder="+34 123 456 789" className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]" />
|
<Phone className="w-3.5 h-3.5 inline mr-1" />
|
||||||
|
Teléfono
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
value={memberData.phone}
|
||||||
|
onChange={(e) => setMemberData({ ...memberData, phone: e.target.value })}
|
||||||
|
placeholder="+34 123 456 789"
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Posición *</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Posición *</label>
|
||||||
<select value={memberData.position} onChange={(e) => setMemberData({ ...memberData, position: e.target.value })} className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]">
|
<select
|
||||||
|
value={memberData.position}
|
||||||
|
onChange={(e) => setMemberData({ ...memberData, position: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
>
|
||||||
<option value="baker">Panadero</option>
|
<option value="baker">Panadero</option>
|
||||||
<option value="pastry-chef">Pastelero</option>
|
<option value="pastry-chef">Pastelero</option>
|
||||||
<option value="manager">Gerente</option>
|
<option value="manager">Gerente</option>
|
||||||
@@ -47,7 +77,11 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Empleo</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Empleo</label>
|
||||||
<select value={memberData.employmentType} onChange={(e) => setMemberData({ ...memberData, employmentType: e.target.value })} className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]">
|
<select
|
||||||
|
value={memberData.employmentType}
|
||||||
|
onChange={(e) => setMemberData({ ...memberData, employmentType: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
>
|
||||||
<option value="full-time">Tiempo Completo</option>
|
<option value="full-time">Tiempo Completo</option>
|
||||||
<option value="part-time">Medio Tiempo</option>
|
<option value="part-time">Medio Tiempo</option>
|
||||||
<option value="contractor">Contratista</option>
|
<option value="contractor">Contratista</option>
|
||||||
@@ -55,13 +89,23 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
||||||
<button onClick={() => { onDataChange({ ...data, ...memberData }); onNext(); }} disabled={!memberData.fullName || !memberData.email} className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 disabled:opacity-50 disabled:cursor-not-allowed">Continuar</button>
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onDataChange({ ...data, ...memberData });
|
||||||
|
onNext();
|
||||||
|
}}
|
||||||
|
disabled={!memberData.fullName || !memberData.email}
|
||||||
|
className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Continuar
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
||||||
|
const { currentTenant } = useTenant();
|
||||||
const [permissions, setPermissions] = useState({
|
const [permissions, setPermissions] = useState({
|
||||||
role: data.role || 'staff',
|
role: data.role || 'staff',
|
||||||
canManageInventory: data.canManageInventory || false,
|
canManageInventory: data.canManageInventory || false,
|
||||||
@@ -69,6 +113,48 @@ const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComp
|
|||||||
canCreateOrders: data.canCreateOrders || false,
|
canCreateOrders: data.canCreateOrders || false,
|
||||||
canViewFinancial: data.canViewFinancial || false,
|
canViewFinancial: data.canViewFinancial || false,
|
||||||
});
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!currentTenant?.id) {
|
||||||
|
setError('No se pudo obtener información del tenant');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate a temporary password (in production, this should be sent via email)
|
||||||
|
const tempPassword = `Temp${Math.random().toString(36).substring(2, 10)}!`;
|
||||||
|
|
||||||
|
// Register the new team member
|
||||||
|
const registrationData = {
|
||||||
|
email: data.email,
|
||||||
|
password: tempPassword,
|
||||||
|
full_name: data.fullName,
|
||||||
|
phone_number: data.phone || undefined,
|
||||||
|
tenant_id: currentTenant.id,
|
||||||
|
role: permissions.role,
|
||||||
|
};
|
||||||
|
|
||||||
|
await authService.register(registrationData);
|
||||||
|
|
||||||
|
// In a real implementation, you would:
|
||||||
|
// 1. Send email with temporary password
|
||||||
|
// 2. Store permissions in a separate permissions table
|
||||||
|
// 3. Link user to tenant with specific role
|
||||||
|
|
||||||
|
onDataChange({ ...data, ...permissions, tempPassword });
|
||||||
|
onComplete();
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Error creating team member:', err);
|
||||||
|
setError(err.response?.data?.detail || 'Error al crear el miembro del equipo');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -77,9 +163,20 @@ const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComp
|
|||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Rol y Permisos</h3>
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Rol y Permisos</h3>
|
||||||
<p className="text-sm text-[var(--text-secondary)]">{data.fullName}</p>
|
<p className="text-sm text-[var(--text-secondary)]">{data.fullName}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-3">Rol del Sistema</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-3">Rol del Sistema</label>
|
||||||
<select value={permissions.role} onChange={(e) => setPermissions({ ...permissions, role: e.target.value })} className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]">
|
<select
|
||||||
|
value={permissions.role}
|
||||||
|
onChange={(e) => setPermissions({ ...permissions, role: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
|
>
|
||||||
<option value="admin">Administrador</option>
|
<option value="admin">Administrador</option>
|
||||||
<option value="manager">Gerente</option>
|
<option value="manager">Gerente</option>
|
||||||
<option value="staff">Personal</option>
|
<option value="staff">Personal</option>
|
||||||
@@ -95,15 +192,39 @@ const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComp
|
|||||||
{ key: 'canCreateOrders', label: 'Crear Pedidos' },
|
{ key: 'canCreateOrders', label: 'Crear Pedidos' },
|
||||||
{ key: 'canViewFinancial', label: 'Ver Datos Financieros' },
|
{ key: 'canViewFinancial', label: 'Ver Datos Financieros' },
|
||||||
].map(({ key, label }) => (
|
].map(({ key, label }) => (
|
||||||
<label key={key} className="flex items-center gap-3 p-3 border border-[var(--border-secondary)] rounded-lg cursor-pointer hover:bg-[var(--bg-secondary)]/30">
|
<label
|
||||||
<input type="checkbox" checked={permissions[key as keyof typeof permissions] as boolean} onChange={(e) => setPermissions({ ...permissions, [key]: e.target.checked })} className="w-4 h-4 text-[var(--color-primary)] rounded focus:ring-[var(--color-primary)]" />
|
key={key}
|
||||||
|
className="flex items-center gap-3 p-3 border border-[var(--border-secondary)] rounded-lg cursor-pointer hover:bg-[var(--bg-secondary)]/30"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={permissions[key as keyof typeof permissions] as boolean}
|
||||||
|
onChange={(e) => setPermissions({ ...permissions, [key]: e.target.checked })}
|
||||||
|
className="w-4 h-4 text-[var(--color-primary)] rounded focus:ring-[var(--color-primary)]"
|
||||||
|
/>
|
||||||
<span className="text-sm text-[var(--text-primary)]">{label}</span>
|
<span className="text-sm text-[var(--text-primary)]">{label}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
|
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
|
||||||
<button onClick={() => { onDataChange({ ...data, ...permissions }); console.log('Saving team member:', { ...data, ...permissions }); onComplete(); }} className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2"><CheckCircle2 className="w-5 h-5" />Agregar Miembro</button>
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
|
Guardando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle2 className="w-5 h-5" />
|
||||||
|
Agregar Miembro
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user