Feat ERP 3

This commit is contained in:
2026-02-21 19:23:17 -03:00
parent 29aa8e30e7
commit 841cc961b5
13 changed files with 835 additions and 26 deletions

View File

@@ -0,0 +1,186 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { X, Save, User, MapPin, Mail, Phone, CreditCard, FileText } from 'lucide-react';
import CuitInput from '../Shared/CuitInput';
import { clientService, type CreateClientRequest } from '../../services/clientService';
import { useToast } from '../../context/use-toast';
import clsx from 'clsx';
interface Props {
onClose: () => void;
onSuccess: (client: { id: number; name: string; dniOrCuit: string }) => void;
initialCuit?: string; // Por si el cajero buscó un CUIT y no existía, precargarlo
}
export default function ClientCreateModal({ onClose, onSuccess, initialCuit = '' }: Props) {
const { showToast } = useToast();
const [loading, setLoading] = useState(false);
// Estado del formulario
const [formData, setFormData] = useState<CreateClientRequest>({
name: '',
dniOrCuit: initialCuit,
email: '',
phone: '',
address: '',
taxType: 'Consumidor Final'
});
const [isCuitValid, setIsCuitValid] = useState(!!initialCuit);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!isCuitValid) return showToast("El CUIT ingresado no es válido", "error");
setLoading(true);
try {
const newClient = await clientService.create(formData);
showToast("Cliente creado exitosamente", "success");
onSuccess(newClient);
onClose();
} catch (error: any) {
console.error(error);
const msg = error.response?.data || "Error al crear cliente";
showToast(msg, "error");
} finally {
setLoading(false);
}
};
return (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-[400] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }}
className="bg-white w-full max-w-2xl rounded-[2rem] shadow-2xl overflow-hidden border border-slate-200 flex flex-col"
>
{/* Header */}
<div className="p-6 bg-slate-50 border-b border-slate-100 flex justify-between items-center">
<div>
<h3 className="text-xl font-black text-slate-800 uppercase tracking-tight flex items-center gap-2">
<User className="text-blue-600" /> Alta Rápida de Cliente
</h3>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest ml-8">Registro Express para Facturación</p>
</div>
<button onClick={onClose} className="p-2 hover:bg-white rounded-xl transition-colors text-slate-400"><X /></button>
</div>
<form onSubmit={handleSubmit} className="p-8 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* CUIT - Componente Reutilizable */}
<div className="md:col-span-1">
<CuitInput
label="DNI / CUIT"
value={formData.dniOrCuit}
required
onChange={(val, valid) => {
setFormData({ ...formData, dniOrCuit: val });
setIsCuitValid(valid);
}}
/>
</div>
{/* Condición Fiscal */}
<div className="md:col-span-1 space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1">
<FileText size={12} /> Condición Fiscal
</label>
<select
className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-bold text-sm text-slate-700 appearance-none"
value={formData.taxType}
onChange={e => setFormData({ ...formData, taxType: e.target.value })}
>
<option value="Consumidor Final">Consumidor Final</option>
<option value="IVA Responsable Inscripto">Responsable Inscripto</option>
<option value="Monotributista">Monotributista</option>
<option value="IVA Exento">IVA Exento</option>
<option value="No Alcanzado">No Alcanzado</option>
</select>
</div>
{/* Razón Social */}
<div className="md:col-span-2 space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1">
<CreditCard size={12} /> Razón Social / Nombre Completo <span className="text-rose-500">*</span>
</label>
<input
required
type="text"
className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-black text-slate-800 text-lg uppercase"
placeholder="Ej: EMPRESA S.A. o JUAN PEREZ"
value={formData.name}
onChange={e => setFormData({ ...formData, name: e.target.value })}
/>
</div>
{/* Dirección */}
<div className="md:col-span-2 space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1">
<MapPin size={12} /> Dirección de Facturación
</label>
<input
type="text"
className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-medium text-sm text-slate-700"
placeholder="Calle, Altura, Localidad"
value={formData.address || ''}
onChange={e => setFormData({ ...formData, address: e.target.value })}
/>
</div>
{/* Email */}
<div className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1">
<Mail size={12} /> Correo Electrónico
</label>
<input
type="email"
className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-medium text-sm text-slate-700"
placeholder="contacto@cliente.com"
value={formData.email || ''}
onChange={e => setFormData({ ...formData, email: e.target.value })}
/>
</div>
{/* Teléfono */}
<div className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1">
<Phone size={12} /> Teléfono
</label>
<input
type="text"
className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-medium text-sm text-slate-700"
placeholder="Cod. Area + Número"
value={formData.phone || ''}
onChange={e => setFormData({ ...formData, phone: e.target.value })}
/>
</div>
</div>
{/* Footer Actions */}
<div className="pt-4 flex gap-4 border-t border-slate-100">
<button
type="button"
onClick={onClose}
className="flex-1 py-4 bg-white border-2 border-slate-200 text-slate-500 font-black uppercase text-xs tracking-widest rounded-xl hover:bg-slate-50 transition-all"
>
Cancelar
</button>
<button
type="submit"
disabled={loading || !isCuitValid || !formData.name}
className={clsx(
"flex-[2] py-4 rounded-xl font-black uppercase text-xs tracking-widest shadow-lg transition-all flex items-center justify-center gap-2",
loading || !isCuitValid || !formData.name
? "bg-slate-300 text-white cursor-not-allowed shadow-none"
: "bg-blue-600 text-white shadow-blue-200 hover:bg-blue-700"
)}
>
<Save size={16} /> {loading ? 'Registrando...' : 'Crear Cliente'}
</button>
</div>
</form>
</motion.div>
</div>
);
}

View File

@@ -0,0 +1,178 @@
import { useState, useEffect, useRef } from 'react';
import { Search, User, Plus, X, ArrowRight, CreditCard, ShieldAlert } from 'lucide-react';
import { clientService } from '../../services/clientService';
import { useDebounce } from '../../hooks/useDebounce';
import { motion } from 'framer-motion';
import clsx from 'clsx';
interface ClientSearchResult {
id: number;
name: string;
dniOrCuit: string;
taxType: string;
isCreditBlocked?: boolean; // Si el backend lo devuelve en la búsqueda
}
interface Props {
onClose: () => void;
onSelect: (client: { id: number; name: string }) => void;
onCreateNew: () => void; // Callback para switchear al modal de creación
}
export default function ClientSearchModal({ onClose, onSelect, onCreateNew }: Props) {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const [results, setResults] = useState<ClientSearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
// Carga inicial y búsqueda
useEffect(() => {
setLoading(true);
clientService.getAll(debouncedQuery)
.then((data: any) => {
setResults(data);
setSelectedIndex(0); // Reset selección al buscar
})
.catch(console.error)
.finally(() => setLoading(false));
}, [debouncedQuery]);
// Manejo de Teclado
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex(prev => (prev + 1) % results.length);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex(prev => (prev - 1 + results.length) % results.length);
} else if (e.key === 'Enter') {
e.preventDefault();
if (results.length > 0) {
handleSelect(results[selectedIndex]);
}
} else if (e.key === 'Escape') {
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [results, selectedIndex]);
const handleSelect = (client: ClientSearchResult) => {
onSelect({ id: client.id, name: client.name });
onClose();
};
return (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-[300] flex items-start justify-center pt-20 p-4">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="bg-white w-full max-w-2xl rounded-[2rem] shadow-2xl overflow-hidden border border-slate-200 flex flex-col max-h-[80vh]"
>
{/* Header con Buscador */}
<div className="p-6 border-b border-slate-100 bg-slate-50">
<div className="flex justify-between items-center mb-4">
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest flex items-center gap-2">
<User className="text-blue-600" size={18} /> Buscar Cliente
</h3>
<button onClick={onClose} className="text-slate-400 hover:text-slate-600"><X size={20} /></button>
</div>
<div className="relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" size={20} />
<input
ref={inputRef}
autoFocus
type="text"
placeholder="Escriba Nombre, Razón Social, DNI o CUIT..."
className="w-full pl-12 pr-4 py-4 bg-white border-2 border-slate-200 rounded-xl outline-none focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10 font-bold text-lg text-slate-800 placeholder:text-slate-300 transition-all"
value={query}
onChange={e => setQuery(e.target.value)}
/>
{loading && (
<div className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
)}
</div>
</div>
{/* Lista de Resultados */}
<div className="flex-1 overflow-y-auto p-2 custom-scrollbar bg-white min-h-[300px]">
{results.length === 0 && !loading ? (
<div className="h-full flex flex-col items-center justify-center text-slate-400 gap-3">
<Search size={48} className="opacity-20" />
<p className="text-xs font-bold uppercase tracking-widest">No se encontraron clientes</p>
<button
onClick={onCreateNew}
className="mt-2 bg-blue-600 text-white px-6 py-2 rounded-xl font-black text-xs uppercase tracking-widest hover:bg-blue-700 transition-all flex items-center gap-2"
>
<Plus size={16} /> Crear Nuevo Cliente
</button>
</div>
) : (
<div className="space-y-1">
{results.map((client, idx) => (
<div
key={client.id}
onClick={() => handleSelect(client)}
className={clsx(
"p-4 rounded-xl cursor-pointer transition-all flex justify-between items-center group",
idx === selectedIndex
? "bg-blue-600 text-white shadow-md transform scale-[1.01]"
: "hover:bg-slate-50 text-slate-700"
)}
>
<div className="flex items-center gap-4">
<div className={clsx(
"w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm",
idx === selectedIndex ? "bg-white/20 text-white" : "bg-slate-100 text-slate-500"
)}>
{client.name.charAt(0).toUpperCase()}
</div>
<div>
<div className="font-black uppercase text-sm leading-tight">{client.name}</div>
<div className={clsx("text-[10px] font-mono mt-1 flex items-center gap-2", idx === selectedIndex ? "text-blue-100" : "text-slate-400")}>
<CreditCard size={10} /> {client.dniOrCuit}
<span className="opacity-50"></span>
<span>{client.taxType || 'Consumidor Final'}</span>
</div>
</div>
</div>
{/* Indicador visual si está seleccionado o bloqueado */}
<div className="flex items-center gap-3">
{client.isCreditBlocked && (
<div className={clsx("px-2 py-1 rounded text-[8px] font-black uppercase flex items-center gap-1", idx === selectedIndex ? "bg-rose-500/20 text-white" : "bg-rose-100 text-rose-600")}>
<ShieldAlert size={10} /> Bloqueado
</div>
)}
<ArrowRight size={18} className={clsx("transition-transform", idx === selectedIndex ? "opacity-100 translate-x-0" : "opacity-0 -translate-x-2")} />
</div>
</div>
))}
</div>
)}
</div>
{/* Footer con Ayuda */}
<div className="p-3 bg-slate-50 border-t border-slate-100 flex justify-between items-center text-[9px] font-bold text-slate-400 uppercase tracking-wider px-6">
<div className="flex gap-4">
<span> Navegar</span>
<span>ENTER Seleccionar</span>
</div>
<button
onClick={onCreateNew}
className="flex items-center gap-1 hover:text-blue-600 transition-colors"
>
<Plus size={12} /> Crear Nuevo Cliente
</button>
</div>
</motion.div>
</div>
);
}

View File

@@ -0,0 +1,98 @@
import { useState, useEffect, type ChangeEvent } from 'react';
import { isValidCuit, formatCuit } from '../../utils/cuitValidator';
import { ShieldCheck, ShieldAlert } from 'lucide-react';
import clsx from 'clsx';
interface CuitInputProps {
value: string;
onChange: (value: string, isValid: boolean) => void;
required?: boolean;
placeholder?: string;
className?: string;
label?: string; // Opcional, para renderizar el label dentro del componente
}
export default function CuitInput({
value,
onChange,
required = false,
placeholder = "20-12345678-9",
className,
label
}: CuitInputProps) {
const [isValid, setIsValid] = useState(true);
const [isTouched, setIsTouched] = useState(false);
// Validar al montar o cuando cambia el value externamente
useEffect(() => {
if (value) {
setIsValid(isValidCuit(value));
} else {
setIsValid(!required); // Si está vacío y no es requerido, es válido
}
}, [value, required]);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const rawValue = e.target.value;
const formatted = formatCuit(rawValue);
// Solo permitir hasta 13 caracteres (11 dígitos + 2 guiones)
if (formatted.length > 13) return;
const valid = isValidCuit(formatted);
setIsTouched(true);
setIsValid(valid);
// Propagamos el cambio al padre
onChange(formatted, valid);
};
const handleBlur = () => {
setIsTouched(true);
};
const showError = isTouched && !isValid && value.length > 0;
const showSuccess = isValid && value.length === 13;
return (
<div className={className}>
{label && (
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 mb-1.5 flex items-center gap-1">
{label}
{required && <span className="text-rose-500">*</span>}
</label>
)}
<div className="relative">
<input
type="text"
value={value}
onChange={handleChange}
onBlur={handleBlur}
placeholder={placeholder}
required={required}
className={clsx(
"w-full p-3 bg-slate-50 border-2 rounded-xl outline-none font-mono font-bold text-sm transition-all pr-10",
showError
? "border-rose-300 text-rose-700 focus:border-rose-500 bg-rose-50"
: showSuccess
? "border-emerald-300 text-emerald-700 focus:border-emerald-500 bg-emerald-50/30"
: "border-slate-100 text-slate-700 focus:border-blue-500"
)}
/>
{/* Icono de estado absoluto a la derecha */}
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
{showError && <ShieldAlert size={18} className="text-rose-500 animate-pulse" />}
{showSuccess && <ShieldCheck size={18} className="text-emerald-500" />}
</div>
</div>
{showError && (
<p className="text-[10px] font-bold text-rose-500 mt-1 ml-1 uppercase tracking-tight">
CUIT inválido (Verifique dígito verificador)
</p>
)}
</div>
);
}

View File

@@ -54,7 +54,7 @@ export default function FastEntryPage() {
const catWrapperRef = useRef<HTMLDivElement>(null);
const [formData, setFormData] = useState({
categoryId: '', operationId: '', text: '', title: '', price: '', days: 3, clientName: '', clientDni: '',
categoryId: '', operationId: '', text: '', title: '', price: '', days: 3, clientName: '', clientDni: '', clientId: null as number | null,
startDate: new Date(Date.now() + 86400000).toISOString().split('T')[0],
isFeatured: false, allowContact: false
});
@@ -260,7 +260,7 @@ export default function FastEntryPage() {
}
}, [debouncedClientSearch, showSuggestions]);
const handlePaymentConfirm = async (payments: Payment[]) => {
const handlePaymentConfirm = async (payments: Payment[], _isCreditSale: boolean) => {
try {
const listingRes = await api.post('/listings', {
categoryId: parseInt(formData.categoryId),
@@ -308,6 +308,7 @@ export default function FastEntryPage() {
price: '',
clientName: '',
clientDni: '',
clientId: null,
startDate: new Date(Date.now() + 86400000).toISOString().split('T')[0],
isFeatured: false,
allowContact: false
@@ -324,7 +325,7 @@ export default function FastEntryPage() {
};
const handleSelectClient = (client: Client) => {
setFormData(prev => ({ ...prev, clientName: client.name, clientDni: client.dniOrCuit }));
setFormData(prev => ({ ...prev, clientName: client.name, clientDni: client.dniOrCuit, clientId: client.id }));
setShowSuggestions(false);
};
@@ -649,7 +650,7 @@ export default function FastEntryPage() {
</motion.div>
{showPaymentModal && (
<PaymentModal totalAmount={pricing.totalPrice} onConfirm={handlePaymentConfirm} onCancel={() => setShowPaymentModal(false)} />
<PaymentModal totalAmount={pricing.totalPrice} clientId={formData.clientId} onConfirm={handlePaymentConfirm} onCancel={() => setShowPaymentModal(false)} />
)}
</>
);

View File

@@ -9,8 +9,9 @@ import PaymentModal, { type Payment } from '../components/PaymentModal';
import { orderService } from '../services/orderService';
import type { CreateOrderRequest } from '../types/Order';
import AdEditorModal from '../components/POS/AdEditorModal';
// Importamos el componente de búsqueda de clientes para el modal (Asumiremos que existe o usamos un simple prompt por ahora para no extender demasiado, idealmente ClientSearchModal)
// import ClientSearchModal from '../components/POS/ClientSearchModal';
import ClientCreateModal from '../components/POS/ClientCreateModal';
import ClientSearchModal from '../components/POS/ClientSearchModal';
import { AnimatePresence } from 'framer-motion';
export default function UniversalPosPage() {
const { showToast } = useToast();
@@ -23,6 +24,8 @@ export default function UniversalPosPage() {
// Estados de Modales
const [showAdEditor, setShowAdEditor] = useState(false);
const [selectedAdProduct, setSelectedAdProduct] = useState<Product | null>(null);
const [showCreateClient, setShowCreateClient] = useState(false);
const [showClientSearch, setShowClientSearch] = useState(false);
// Estado de carga para agregar combos (puede tardar un poco en traer los hijos)
const [addingProduct, setAddingProduct] = useState(false);
@@ -56,17 +59,6 @@ export default function UniversalPosPage() {
return () => window.removeEventListener('keydown', handleKeyDown);
}, [items, clientId]); // Dependencias para que handleCheckout tenga el estado fresco
const handleChangeClient = () => {
// Aquí abriríamos el ClientSearchModal.
// Para no bloquear, simulamos un cambio rápido o un prompt simple si no hay modal aún.
// En producción: setShowClientModal(true);
const id = prompt("Ingrese ID de Cliente (Simulación F7):", "1003");
if (id) {
// Buscar nombre real en API...
setClient(parseInt(id), "Cliente #" + id);
}
};
const handleProductSelect = async (product: Product) => {
setAddingProduct(true);
try {
@@ -122,6 +114,29 @@ export default function UniversalPosPage() {
setShowPayment(true);
};
const handleChangeClient = () => {
setShowClientSearch(true);
};
// Callback cuando seleccionan del buscador
const handleClientSelected = (client: { id: number; name: string }) => {
setClient(client.id, client.name);
// showClientSearch se cierra automáticamente por el componente, o lo forzamos aquí si es necesario
// El componente ClientSearchModal llama a onClose internamente después de onSelect
};
// Callback cuando crean uno nuevo
const handleClientCreated = (client: { id: number; name: string }) => {
setClient(client.id, client.name);
setShowCreateClient(false);
};
// Función puente: Del Buscador -> Al Creador
const switchToCreate = () => {
setShowClientSearch(false);
setTimeout(() => setShowCreateClient(true), 100); // Pequeño delay para transición suave
};
const finalizeOrder = async (_payments: Payment[], isCreditSale: boolean) => {
setIsProcessing(true);
try {
@@ -297,6 +312,27 @@ export default function UniversalPosPage() {
clientId={clientId || 1005}
/>
)}
{/* MODAL DE BÚSQUEDA DE CLIENTE (F7) */}
<AnimatePresence>
{showClientSearch && (
<ClientSearchModal
onClose={() => setShowClientSearch(false)}
onSelect={handleClientSelected}
onCreateNew={switchToCreate}
/>
)}
</AnimatePresence>
{/* MODAL DE ALTA RÁPIDA */}
<AnimatePresence>
{showCreateClient && (
<ClientCreateModal
onClose={() => setShowCreateClient(false)}
onSuccess={handleClientCreated}
/>
)}
</AnimatePresence>
</div>
);
}

View File

@@ -0,0 +1,33 @@
// src/services/clientService.ts
import api from './api';
export interface CreateClientRequest {
name: string;
dniOrCuit: string;
email?: string;
phone?: string;
address?: string;
taxType: string;
}
export const clientService = {
getAll: async (q?: string) => {
const res = await api.get('/clients', { params: { q } });
return res.data;
},
getSummary: async (id: number) => {
const res = await api.get(`/clients/${id}/summary`);
return res.data;
},
update: async (id: number, clientData: any) => {
await api.put(`/clients/${id}`, clientData);
},
resetPassword: async (id: number) => {
const res = await api.post(`/clients/${id}/reset-password`);
return res.data;
},
create: async (data: CreateClientRequest) => {
const res = await api.post('/clients', data);
return res.data; // Retorna { id, name, dniOrCuit }
}
};

View File

@@ -0,0 +1,43 @@
/**
* Valida un CUIT/CUIL argentino aplicando el algoritmo de Módulo 11.
* Soporta formatos con o sin guiones (20-12345678-9 o 20123456789).
*/
export const isValidCuit = (cuit: string): boolean => {
if (!cuit) return false;
// 1. Limpiar el input: dejar solo números
const cleanCuit = cuit.replace(/[^0-9]/g, '');
// 2. Verificar longitud exacta (11 dígitos)
if (cleanCuit.length !== 11) return false;
// 3. Convertir a array de números
const digits = cleanCuit.split('').map(Number);
// 4. Algoritmo de verificación (Módulo 11)
const multipliers = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
let total = 0;
for (let i = 0; i < 10; i++) {
total += digits[i] * multipliers[i];
}
const remainder = total % 11;
const calculatedVerifier = remainder === 0 ? 0 : remainder === 1 ? 9 : 11 - remainder;
const actualVerifier = digits[10];
// Caso especial: Cuando el resto es 1, el resultado suele ser 9 en algunos casos específicos
// o se considera inválido y se debe cambiar el prefijo (Hombres/Mujeres),
// pero para validación estricta matemática:
return calculatedVerifier === actualVerifier;
};
/**
* Formatea un string de números al formato XX-XXXXXXXX-X
*/
export const formatCuit = (cuit: string): string => {
const nums = cuit.replace(/[^0-9]/g, '');
if (nums.length <= 2) return nums;
if (nums.length <= 10) return `${nums.slice(0, 2)}-${nums.slice(2)}`;
return `${nums.slice(0, 2)}-${nums.slice(2, 10)}-${nums.slice(10, 11)}`;
};