All files / src/components/QuantityInput QuantityInput.tsx

100% Statements 46/46
100% Branches 34/34
100% Functions 10/10
100% Lines 46/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175                                            76x     76x     76x 31x       76x 35x 9x             76x 60x   14x       46x 2x       44x     44x 2x       42x 23x       19x 7x     7x 2x       7x 1x       7x     7x   7x       19x 19x 13x 13x           76x 18x   1x 1x   17x 17x 16x         76x 3x   76x 3x     76x                                                 3x 1x   3x 2x                                                    
import { useState, useEffect, useRef } from 'react';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import TextField from '@mui/material/TextField';
import RemoveIcon from '@mui/icons-material/Remove';
import AddIcon from '@mui/icons-material/Add';
import type { CartItem } from '../../stores/useCartStore';
 
interface QuantityInputProps {
  item: CartItem;
  adjustQty: (item: CartItem, newQty: number) => void;
  debounceMs?: number;
  disabled?: boolean;
}
 
export default function QuantityInput({
  item,
  adjustQty,
  debounceMs = 500, // Délai par défaut (ms) avant d'envoyer la mise à jour
  disabled = false,
}: QuantityInputProps) {
  // Valeur locale du champ, sous forme de chaîne
  const [inputValue, setInputValue] = useState<string>(item.quantity.toString());
 
  // Référence pour stocker l'ID du timer de debounce
  const debounceRef = useRef<number | null>(null);
 
  // Si item.quantity change (suite à un clic sur +/– par exemple), on synchronise inputValue
  useEffect(() => {
    setInputValue(item.quantity.toString());
  }, [item.quantity]);
 
  // Si disabled passe à true, on peut remettre inputValue à item.quantity immédiatement pour cohérence.
  useEffect(() => {
    if (disabled) {
      setInputValue(item.quantity.toString());
    }
    // On ne met pas item.quantity en dépendance ici car un autre useEffect gère déjà la synchro.
  }, [disabled, item.quantity]);
  
 
  // Effet : à chaque modification de inputValue, on démarre (ou on reset) le debounce
  useEffect(() => {
    if (disabled) {
      // Ne rien faire quand disabled
      return;
    }
 
    // Si l'utilisateur a complètement vidé le champ, on ne déclenche rien
    if (inputValue === '') {
      return;
    }
 
    // Convertir en nombre
    const rawVal = Number(inputValue);
 
    // Si ce n'est pas un nombre valide, on ne déclenche pas non plus
    if (isNaN(rawVal)) {
      return;
    }
 
    // Si la valeur (rawVal) est identique à item.quantity, on n'a pas besoin d'actualiser
    if (rawVal === item.quantity) {
      return;
    }
 
    // On démarre un nouveau timer
    debounceRef.current = window.setTimeout(() => {
      let newQty = rawVal;
 
      // Clamp : si la valeur est négative, on met 0
      if (newQty < 0) {
        newQty = 0;
      }
 
      // Clap : si la valeur dépasse le stock, on met availableQuantity
      if (newQty > item.availableQuantity) {
        newQty = item.availableQuantity;
      }
 
      // On applique la mise à jour
      adjustQty(item, newQty);
 
      // On met à jour inputValue pour refléter le clamp
      setInputValue(newQty.toString());
 
      debounceRef.current = null;
    }, debounceMs);
 
    // Cleanup à chaque nouvelle exécution ou démontage
    return () => {
      if (debounceRef.current) {
        clearTimeout(debounceRef.current);
        debounceRef.current = null;
      }
    };
  }, [inputValue, item, adjustQty, debounceMs, disabled]);
 
  // Gère la saisie utilisateur (on accepte uniquement chiffres ou chaîne vide)
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (disabled) {
      // On remet la valeur précédente pour éviter saisie visuelle erronée
      setInputValue(item.quantity.toString());
      return;
    }
    const raw = e.target.value;
    if (raw === '' || /^[0-9]+$/.test(raw)) {
      setInputValue(raw);
    }
  };
 
  // Les boutons +/- qui appellent adjustQty immédiatement
  const increment = () => {
    adjustQty(item, Math.min(item.quantity + 1, item.availableQuantity));
  };
  const decrement = () => {
    adjustQty(item, Math.max(item.quantity - 1, 0));
  };
 
  return (
    <Box
      sx={{
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        gap: 1,
      }}
    >
      <IconButton 
        size="small" 
        onClick={decrement} 
        disabled={disabled || item.quantity <= 0}
        aria-label="decrement quantity"
      >
        <RemoveIcon fontSize="small" />
      </IconButton>
 
      <TextField
        type="text"
        size="small"
        value={inputValue}
        onChange={handleChange}
        onBlur={() => {
          // Au blur, si champ vide, on remet la vraie quantité
          if (!disabled && inputValue === '') {
            setInputValue(item.quantity.toString());
          }
          if (disabled) {
            setInputValue(item.quantity.toString());
          }
        }}
        slotProps={{
          input: {
            inputProps: {
              inputMode: 'numeric', 
              pattern: '[0-9]*',    
              style: { textAlign: 'center', width: 40 },
            },
          },
        }}
        disabled={disabled}
      />
 
      <IconButton
        size="small"
        onClick={increment}
        aria-label="increment quantity"
        disabled={disabled || item.quantity >= item.availableQuantity}
      >
        <AddIcon fontSize="small" />
      </IconButton>
    </Box>
  );
}