How can i upload image on Laravel React App

31 Views Asked by At

I want to implement a function that allows the users to upload image. The validation returns "Trying to access array offset on value of type null" The selection of image is done in this component

import React, { useState, useRef } from 'react';

const AddImages = ({ onImagesChange }) => {
  const [images, setImages] = useState([]);
  const inputRef = useRef(null);

  const handleImageChange = (event) => {
    const selectedImages = Array.from(event.target.files);
    setImages(selectedImages);
    onImagesChange(selectedImages); // Passing selected images to the parent component
  };

  const handleDrop = (event) => {
    event.preventDefault();
    const droppedFiles = Array.from(event.dataTransfer.files);
    setImages(droppedFiles);
    onImagesChange(droppedFiles); // Passing dropped images to the parent component
    if (inputRef.current) {
      inputRef.current.value = null;
    }
  };

  const handleDragOver = (event) => {
    event.preventDefault();
  };

  const handleRemoveImage = (index) => {
    const updatedImages = images.filter((_, i) => i !== index);
    setImages(updatedImages);
    onImagesChange(updatedImages); // Passing updated images to the parent component
  };

  const handleClick = () => inputRef.current && inputRef.current.click();

  return (
    <div className='w-100%' style={{ textAlign: 'center' }}>
      <div
        className='p-5 border-dashed border-2 border-sky-500 w-100%'
        style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}
        onDrop={handleDrop}
        onDragOver={handleDragOver}
      >
        <input
          ref={inputRef}
          type="file"
          id="imageInput"
          accept="image/*"
          multiple
          onChange={handleImageChange}
          style={{ display: 'none', width: '100%' }}
        />
        <button type="button" onClick={handleClick}>Choose Image</button>
        {images.length > 0 && (
          <div>
            {images.map((image, index) => (
              <div key={index} style={{ marginBottom: '10px' }}>
                <img src={URL.createObjectURL(image)} alt={`Selected Image ${index}`} style={{ maxWidth: '100px', maxHeight: '100px' }} />
                <button onClick={() => handleRemoveImage(index)}>Remove</button>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

export default AddImages;

the axios request is made in this compont

import React, { useState, useEffect } from 'react';
import Submit from '../../../../../../components/buttons/Submit';
import NeutralButton from '../../../../../../components/buttons/NeutralButton';
import { TemplateHandler } from 'easy-template-x';
import axiosClient from '../../../../../../axios/axios';
import InsetEmployeeAccomplishmentReport from '../../../../../../components/printing/forms/InsetEmployeeAccomplishmentReport.docx'
import Addimages from '../../../../../../components/image/Addimages';

//For Feedback
import Feedback from '../../../../../../components/feedbacks/Feedback';
import { MinusCircleIcon } from '@heroicons/react/20/solid';

export default function GenerateFormReport({ selectedForm }) {
  
  const [formData, setFormData] = useState({
    forms_id: selectedForm.id,
    title: selectedForm.title,
    fund_source: 'n/a',
    clientele_type: 'n/a',
    clientele_number: 'n/a',
    actual_cost: 'n/a',
    cooperating_agencies_units: 'n/a',
    ...(selectedForm.form_type !== "INSET" && { date_of_activity: selectedForm.date_of_activity }),
    ...(selectedForm.form_type === "INSET" && { date_of_activity: selectedForm.date_of_activity }),
    venue: selectedForm.venue,
    no_of_participants: '',
    male_participants: '',
    female_participants: '',
    proponents_implementors: selectedForm.proponents_implementors,
    images: [], //for images
  });

  const [actualExpendatures, setActualExpendatures] = useState([{
    type: '',
    item: '',
    approved_budget: '',
    actual_expenditure: '',
  }]);

  const handleImagesChange = (selectedImages) => {
    // Update the formData state with the array of selected images
    setFormData(prevFormData => ({
      ...prevFormData,
      images: selectedImages
    }));
  };
  
  // useEffect to log updated formData state
  useEffect(() => {
    console.log('Updated FormData with images:', formData);
  }, [formData]); // Trigger the effect whenever formData changes
  
  const expendituresArray = selectedForm.expenditures;

  const [proposedExpenditures, setProposedExpenditures] = useState([
    {type: '', item: '', per_item: '', no_item: '', times: '', total: ''}
  ]);

  //----------for docx
  const fileUrl = InsetEmployeeAccomplishmentReport; // Use the imported file directly

  const fetchData = async (url) => {
    const response = await fetch(url);
    return await response.blob();
  };

  const populateDocx = async () => {
    try {
        const blob = await fetchData(fileUrl);
         const data = {
            title: formData.title,
            dateOfActivity: formData.date_of_activity,
            venue: formData.venue,
            proponents: formData.proponents_implementors,
            maleParticipants: formData.male_participants,
            femaleParticipants: formData.female_participants,
            totalParticipants: formData.no_of_participants,
            // Include additional fields here as needed
            // For example, for budgetary requirements
            budgetaryExpenditure: actualExpendatures.map(field => ({
              item: field.item,
              approvedBudget: field.approved_budget,
              actualExpenditure: field.actual_expenditure
          }))
        };
        
        const handler = new TemplateHandler();
        const processedBlob = await handler.process(blob, data); // Process the blob
        saveFile('output.docx', processedBlob, data.title);
    } catch (error) {
        console.error('Error:', error);
    }
  };

    const saveFile = (filename, blob, title) => {
      try {
        const blobUrl = window.URL.createObjectURL(blob);
        const link = document.createElement("a");
        link.href = blobUrl;
        link.download = `${title} - ${filename}`; // Include the title in the filename
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link); // Clean up the DOM
        window.URL.revokeObjectURL(blobUrl);
      } catch (error) {
        console.error('Error creating object URL:', error);
      }
    };
  //----------

  //------------------------------
  useEffect(() => {
    // Function to generate multiple sets of input fields
    const generateInputFields = () => {
      const newInputFields = expendituresArray.map(expenditure => ({
        id: expenditure.id,
        type: expenditure.type,
        item: expenditure.items,
        per_item: expenditure.per_item,
        no_item: expenditure.no_item,
        times: expenditure.times,
        total: expenditure.total
      }));
      setProposedExpenditures(newInputFields);
    };
  
    generateInputFields();
}, []);
  //------------------------------

  //For feedback
  const [error, setError] = useState('');
  const [message, setAxiosMessage] = useState(''); // State for success message
  const [status, setAxiosStatus] = useState('');
  
  const handleSubmit = async (ev) => {
    ev.preventDefault();
    setError({ __html: "" });
  
    // Create FormData object
    const formDataToSend = new FormData();
  
    // Append form data
    for (const key in formData) {
      formDataToSend.append(key, formData[key]);
    }
  
    // Append image files
    formData.images.forEach((image, index) => {
      formDataToSend.append(`images[${index}]`, image);
    });
  
    // Append expenditures data
    formDataToSend.append('expenditures', JSON.stringify(actualExpendatures));
    
    console.log('formDataToSend',formDataToSend);

    try {
      const response = await axiosClient.post('/accomplishment_report', formDataToSend);
      setAxiosMessage(response.data.message); // Set success message
      setAxiosStatus(response.data.success);
      if (response.data.success === true){
        populateDocx(); // Run the download of DOCX
      }
      setTimeout(() => {
        setAxiosMessage(''); // Clear success message
        setAxiosStatus('');
      }, 3000); // Timeout after 3 seconds
    } catch (error) {
      setAxiosMessage(error.response.data.message); // Set success message
    }
  };
  
  const handleFormChange = (index, event) => {
    let data = [...actualExpendatures];
    data[index][event.target.name] = event.target.value;
    setActualExpendatures(data);
  }
  
  const addFields = () => {
    let newfield = { type: '', item: '', approved_budget: '', actual_expenditure: '' }
    setActualExpendatures([...actualExpendatures, newfield])
    //will also add to DB
  }
  
  const removeFields = (index) => {
    let data = [...actualExpendatures];
    data.splice(index, 1)
    setActualExpendatures(data)
    //will also remove from DB
  }

  const handleChange = async (e) => {
    setFormData({ ...formData, [e.target.name]: e.target.value });
  };

// For Unified Inputs 
const renderInput = (name, label) => {
  // Check if the input field should be required based on form type and field name
  const isRequired = selectedForm.form_type !== "INSET" && name == "no_of_target_participants";

  console.log('test',formData.images);
  return (
    <div className='flex flex-1 flex-col'>
      
    {/* Integrate the Success component */}
    <Feedback isOpen={message !== ''} onClose={() => setAxiosMessage('')} successMessage={message}  status={status}/>

      <label htmlFor={name}>{label}</label>
      <input
        id={name}
        name={name}
        type="text"
        autoComplete={name}
        placeholder="I am empty..."
        required
        // Include "required" attribute only if it's not INSET and not no_of_target_participants
        //{...(isRequired ? { required: true } : {})}
        value={formData[name]}
        onChange={handleChange}
        className="bg-gray-100"
      />
    </div>
  );
};

  return (
    <div>
      {error.__html && (
        <div
          className="bg-red-500 rounded py-2 px-3 text-white"
          dangerouslySetInnerHTML={error}
        ></div>
      )}

    <form onSubmit={handleSubmit} className="flex flex-1 flex-col" encType="multipart/form-data">
      {renderInput("title", "Title: ")}
      {renderInput(selectedForm.form_type === "INSET" ? "date_of_activity" : "date_of_activity", "Date of Activity: ")}
      {renderInput("venue", "Venue: ")}
      {renderInput("proponents_implementors", "Proponents/Implementors ")}
      {renderInput("male_participants", "Male Participants: ")}
      {renderInput("female_participants", "Female Participants: ")}
      {renderInput("no_of_participants", "Total Number of Participants: ")}

      <h1 className='text-center m-3'>
        Proposed Expenditures:
      </h1>
      <div className="overflow-x-auto">
        <table className="min-w-full divide-y divide-gray-200">
          <thead>
            <tr>
              <th className="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium uppercase tracking-wider">Item Type</th>
              <th className="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium uppercase tracking-wider">Item</th>
              <th className="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium uppercase tracking-wider">Cost Per Item</th>
              <th className="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium uppercase tracking-wider">No. of Items</th>
              <th className="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium uppercase tracking-wider">X Times</th>
              <th className="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium uppercase tracking-wider">Total</th>
            </tr>
          </thead>
          <tbody className="bg-white divide-y divide-gray-200">
            {proposedExpenditures.map((input, index) => (
              <tr key={index}>
                <td className="px-6 py-4 whitespace-no-wrap">{input.type}</td>
                <td className="px-6 py-4 whitespace-no-wrap">{input.item}</td>
                <td className="px-6 py-4 whitespace-no-wrap">{input.per_item}</td>
                <td className="px-6 py-4 whitespace-no-wrap">{input.no_item}</td>
                <td className="px-6 py-4 whitespace-no-wrap">{input.times}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <h1 className='text-center m-3'>
        Actual Expenditures:
      </h1>
      <div className="flex flex-col justify-center items-center w-full overflow-x-auto">
        {/*------------------------------------------------------------------------------*/}
        <table>
          <thead>
            <tr>
              <th>Type</th>
              <th>Item Description</th>
              <th>Approved Budget</th>
              <th>Actual Expendatures</th>
            </tr>
          </thead>
          <tbody>
            {actualExpendatures.map((input, index) => (
              <tr key={index}>
                <td>
                  <select
                    id={`type${index}`}
                    name="type"
                    autoComplete="type"
                    required
                    className="flex-1 px-2 py-1"
                    value={input.type}
                    onChange={event => handleFormChange(index, event)}
                  >
                    <option value="" disabled>Select Type</option>
                    <option value="Meals and Snacks">Meals and Snacks</option>
                    <option value="Function Room/Venue">Venue</option>
                    <option value="Accomodation">Accomodation</option>
                    <option value="Equipment Rental">Equipment Rental</option>
                    <option value="Professional Fee/Honoria">Professional Fee/Honoria</option>
                    <option value="Token/s">Token/s</option>
                    <option value="Materials and Supplies">Materials and Supplies</option>
                    <option value="Transportation">Transportation</option>
                    <option value="Others">Others...</option>
                  </select>
                </td>
                <td>
                  <input
                    id={`item${index}`}
                    name="item"
                    type="text"
                    placeholder="Item"
                    autoComplete="item"
                    required
                    className="flex-1 px-2 py-1 mr-3"
                    value={input.item}
                    onChange={event => handleFormChange(index, event)}
                  />
                </td>
                <td>
                  <input
                    id={`approved_budget${index}`}
                    name="approved_budget"
                    type="text"
                    placeholder="Approved Budget"
                    autoComplete="approved_budget"
                    required
                    className="flex-1 px-2 py-1 mr-3"
                    value={input.approved_budget}
                    onChange={event => handleFormChange(index, event)}
                  />
                </td>
                <td>
                  <input
                    id={`actual_expenditure${index}`}
                    name="actual_expenditure"
                    type="text"
                    placeholder="Actual Expenditure"
                    autoComplete="actual_expenditure"
                    required
                    className="flex-1 px-2 py-1 mr-3"
                    value={input.actual_expenditure}
                    onChange={event => handleFormChange(index, event)}
                  />
                </td>
                  <td className='text-center'>
                    <button type="button" title="Delete Row" onClick={() => removeFields(index)}>
                      <MinusCircleIcon className="w-6 h-6 text-red-500 cursor-pointer transform transition-transform hover:scale-125" />
                    </button>
                  </td>
              </tr>
            ))}
          </tbody>
        </table>
        
          {/*------------------------------------------------------------------------------*/}
          <div className="flex justify-center">

            <NeutralButton label="Add more.." onClick={() => addFields()} />
          {/* <button onClick={addFields} className='m-1'>Add More..</button> */}
          </div>
        
        </div>
        
        <div className='flex justify-center mt-5'>
          <Addimages onImagesChange={handleImagesChange} />
        </div>

        <div className="mt-5">
          <Submit label="Submit"/>
        </div>
      </form>

    </div>
  );
}

here is my validation

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ACReportRequest_E_I extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'forms_id' => 'required|integer',
            'title' => 'required|string',
            'date_of_activity' => 'required|string',
            'venue' => 'required|string',
            'proponents_implementors' => 'required|string',
            'male_participants' => 'nullable|numeric',
            'female_participants' => 'nullable|numeric',
            'no_of_participants' => 'nullable|numeric',

            'images' => 'required|array',
            'images.*' => 'image|mimes:jpeg,png,jpg,gif|max:5120',

            'expenditures.*.type' => 'required|string',
            'expenditures.*.item' => 'required|string',
            'expenditures.*.approved_budget' => 'required|string',
            'expenditures.*.actual_expenditure' => 'required|string',
        ];
    }
}

here is my controller

public function index_accomplishment_report() {
        $accomplishmentReport = accReport::with('actualExpenditure')->get();

        return response($accomplishmentReport);
    }

    public function index_expenditures($id)
    {
        $forms_id = $id;
        //$form = Expenditures::find($forms_id);
        $xps = Expenditures::where('forms_id', $forms_id)->get();

        return response($forms_id);
    }

    public function accomplishment_report_store(ACReportRequest_E_I $request) {
        $accReport = $request->validated('accReport');
        $expenditures = $request->validated('expenditures');
        $images = $accReport['images'];

        // Create Accomplishment Report
        $createdAccReport = accReport::create([
            'forms_id' => $accReport['forms_id'],
            'title' => $accReport['title'],
            'date_of_activity' => $accReport['date_of_activity'],
            'venue' => $accReport['venue'],
            'no_of_participants' => $accReport['no_of_participants'],
            'male_participants' => $accReport['male_participants'],
            'female_participants' => $accReport['female_participants'],
            'focus' => '0',
        ]);
    
        // Find the first item with the given title
        $firstItem = accReport::where('title', $accReport['title'])->first();
     
        // Save Actual Expenditures
        foreach ($expenditures as $expenditure) {
            ActualExpendature::create([
                'acc_report_id' => $firstItem->id,
                'type' => $expenditure['type'],
                'items' => $expenditure['item'],
                'approved_budget' => $expenditure['approved_budget'],
                'actual_expenditure' => $expenditure['actual_expenditure'],
            ]);
        }
    
        // Store images
        $imageModels = [];
        foreach ($images as $image) {
            // Store each image in the storage directory
            $storedImagePath = Storage::put('images/', $image['name']);

            // Concatenate the base URL with the relative path to get the full image path
            $fullImagePath = Storage::url($storedImagePath);

            // Create an array of attributes for each image
            $imageModels[] = ['path' => $fullImagePath];
        }

        // Save the image paths to the database
        $createdAccReport->images()->createMany($imageModels);

        // Return the response with the stored image paths
        return response([
            'success' => true,
            'message' => 'Accomplishment Report Successfully Created',
            'images' => $imageModels  // Return the stored image paths in the response
        ]);

    }

I am stuck in sending the image to the backend

0

There are 0 best solutions below