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