Source: manager/ingredients.jsx

  1. import { useEffect, useState } from 'react';
  2. import { useNavigate } from 'react-router-dom';
  3. /**
  4. * Ingredients Manager Component
  5. *
  6. * Manages the ingredients inventory, allowing users to view, add, and edit ingredients, changing the backend.
  7. *
  8. * @returns {JSX.Element} The rendered Ingredients component.
  9. */
  10. const Ingredients = () => {
  11. const VITE_BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
  12. const [ingredients, setIngredients] = useState([]);
  13. const [isModalOpen, setIsModalOpen] = useState(false);
  14. const [currentIngredient, setCurrentIngredient] = useState(null);
  15. const [ingredientForm, setIngredientForm] = useState({
  16. name: '',
  17. stock: '',
  18. });
  19. const navigate = useNavigate();
  20. /**
  21. * Fetches all ingredients from the backend and updates the state.
  22. *
  23. * @async
  24. * @function loadIngredientsFromDatabase
  25. */
  26. const loadIngredientsFromDatabase = async () => {
  27. try {
  28. const response = await fetch(`${VITE_BACKEND_URL}/api/ingredients/`);
  29. if (!response.ok) {
  30. throw new Error('Failed to fetch ingredients');
  31. }
  32. const data = await response.json();
  33. setIngredients(data);
  34. } catch (error) {
  35. console.error('Error loading ingredients:', error);
  36. }
  37. };
  38. /**
  39. * Handles input changes in the ingredient form.
  40. *
  41. * @param {Object} e - The event object from the input field.
  42. */
  43. const handleInputChange = (e) => {
  44. const { name, value } = e.target;
  45. setIngredientForm((prevForm) => ({
  46. ...prevForm,
  47. [name]: value,
  48. }));
  49. };
  50. /**
  51. * Opens the Add/Edit modal with specified ingredient data or as a blank form.
  52. *
  53. * @param {Object|null} ingredient - The ingredient to edit, or null for a new ingredient.
  54. */
  55. const openModal = (ingredient = null) => {
  56. setCurrentIngredient(ingredient);
  57. setIngredientForm(
  58. ingredient || { name: '', stock: '' }
  59. );
  60. setIsModalOpen(true);
  61. };
  62. /**
  63. * Closes the Add/Edit modal and resets the form.
  64. */
  65. const closeModal = () => {
  66. setIsModalOpen(false);
  67. setCurrentIngredient(null);
  68. setIngredientForm({ name: '', stock: '' });
  69. };
  70. /**
  71. * Submits the form to add or update an ingredient.
  72. *
  73. * @async
  74. * @param {Object} e - The form submit event object.
  75. */
  76. const handleSubmit = async (e) => {
  77. e.preventDefault();
  78. const method = currentIngredient ? 'PUT' : 'POST';
  79. const url = currentIngredient
  80. ? `${VITE_BACKEND_URL}/api/ingredients/update-stock`
  81. : `${VITE_BACKEND_URL}/api/ingredients/create`;
  82. try {
  83. const response = await fetch(url, {
  84. method,
  85. headers: { 'Content-Type': 'application/json' },
  86. body: JSON.stringify(ingredientForm), // Send form data as JSON
  87. });
  88. if (!response.ok) {
  89. throw new Error('Failed to save ingredient');
  90. }
  91. await loadIngredientsFromDatabase(); // Refresh the ingredients list
  92. closeModal(); // Close the modal
  93. } catch (error) {
  94. console.error('Error saving ingredient:', error);
  95. }
  96. };
  97. /**
  98. * Fetches the ingredients when the component is mounted.
  99. *
  100. * @useEffect
  101. */
  102. useEffect(() => {
  103. loadIngredientsFromDatabase();
  104. }, []);
  105. return (
  106. <div className="flex flex-col items-center p-8 bg-gray-50 dark:bg-slate-800">
  107. <button
  108. className="fixed top-20 left-4 bg-gray-300 text-black font-bold text-2xl rounded-full w-12 h-12 flex items-center justify-center bg-opacity-75 hover:scale-110 hover:bg-gray-400 transition-transform duration-200 ease-in-out"
  109. onClick={() => navigate(-1)}
  110. >
  111. {"<"}
  112. </button>
  113. <h1 className="text-4xl font-bold text-red-600 mb-8">Ingredients Inventory</h1>
  114. <button
  115. onClick={() => openModal()}
  116. className="bg-green-600 text-white text-lg px-6 py-2 rounded shadow-lg hover:bg-green-500 transition duration-300 mb-4"
  117. >
  118. Add Ingredient
  119. </button>
  120. <div className="bg-white border border-gray-300 rounded-lg shadow-lg w-full max-w-4xl overflow-hidden">
  121. <table className="min-w-full divide-y divide-gray-200 border-collapse">
  122. <thead className="bg-gray-100">
  123. <tr>
  124. <th className="px-16 py-4 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">
  125. Ingredient
  126. </th>
  127. <th className="px-16 py-4 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">
  128. Stock
  129. </th>
  130. <th className="px-16 py-4 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">
  131. Actions
  132. </th>
  133. </tr>
  134. </thead>
  135. <tbody className="bg-white divide-y dark:bg-slate-300 divide-gray-200">
  136. {ingredients.length === 0 ? (
  137. <tr>
  138. <td colSpan="3" className="px-16 py-4 text-center text-gray-500">
  139. No ingredients found.
  140. </td>
  141. </tr>
  142. ) : (
  143. ingredients.map((ingredient) => (
  144. <tr
  145. key={ingredient.ingredient_id} // Match the backend's "ingredient_id"
  146. className="hover:bg-gray-100 transition duration-300"
  147. >
  148. <td className="px-16 py-4 whitespace-nowrap text-sm font-medium text-gray-900 border-b border-gray-200">
  149. {ingredient.ingredient_name}
  150. </td>
  151. <td className="px-16 py-4 whitespace-nowrap text-sm text-gray-600 border-b border-gray-200">
  152. {ingredient.stock}
  153. </td>
  154. <td className="px-16 py-4 whitespace-nowrap text-sm text-gray-600 border-b border-gray-200">
  155. <button
  156. onClick={() =>
  157. openModal({
  158. name: ingredient.ingredient_name, // Set form data correctly
  159. stock: ingredient.stock,
  160. })
  161. }
  162. className="bg-blue-500 text-white px-4 py-1 rounded hover:bg-blue-400 transition duration-300 mr-2"
  163. >
  164. Edit
  165. </button>
  166. </td>
  167. </tr>
  168. ))
  169. )}
  170. </tbody>
  171. </table>
  172. </div>
  173. {isModalOpen && (
  174. <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
  175. <div className="bg-white dark:bg-black p-6 rounded shadow-lg max-w-sm w-full">
  176. <h2 className="text-xl dark:text-white font-bold mb-4">
  177. {currentIngredient ? 'Edit Ingredient' : 'Add Ingredient'}
  178. </h2>
  179. <form onSubmit={handleSubmit}>
  180. <div className="mb-4">
  181. <label className="block text-gray-700 dark:text-white">Ingredient Name</label>
  182. <input
  183. type="text"
  184. name="name" // Match backend's "name" field
  185. value={ingredientForm.name}
  186. onChange={handleInputChange}
  187. required
  188. className="border rounded w-full p-2"
  189. />
  190. </div>
  191. <div className="mb-4">
  192. <label className="block text-gray-700 dark:text-white">Stock</label>
  193. <input
  194. type="number"
  195. name="stock" // Match backend's "stock" field
  196. value={ingredientForm.stock}
  197. onChange={handleInputChange}
  198. required
  199. className="border rounded w-full p-2"
  200. />
  201. </div>
  202. <div className="flex justify-between">
  203. <button
  204. type="button"
  205. onClick={closeModal}
  206. className="bg-red-500 text-white px-4 py-1 rounded hover:bg-red-400 transition duration-300"
  207. >
  208. Cancel
  209. </button>
  210. <button
  211. type="submit"
  212. className="bg-blue-500 text-white px-4 py-1 rounded hover:bg-blue-400 transition duration-300"
  213. >
  214. {currentIngredient ? 'Update' : 'Add'}
  215. </button>
  216. </div>
  217. </form>
  218. </div>
  219. </div>
  220. )}
  221. </div>
  222. );
  223. };
  224. export default Ingredients;