import React, { useEffect, useState } from 'react';
import FormField from '../ui/FormField';
import Checkbox from '../ui/Checkbox';
import { useAuth } from '../../hooks/useAuth';
/**
* ApproverHierarchyForm
*
* This form lets an admin create / edit an ApproverHierarchy.
* A hierarchy at minimum includes an "is_active" flag and one or more levels.
* We implement a minimal UI that allows an administrator to toggle `is_active` and
* add / remove level rows. Each level row lets the user pick a Role and assign a priority
* (lower number = higher priority). Roles are fetched once from the API so we can populate
* a dropdown.
*/
const emptyLevel = { role: '' };
/**
* Component for managing an Approver Hierarchy form.
* Allows creation and editing of hierarchies including activation status and approval levels.
*
* @param {object} props - The component props.
* @param {object} [props.initialData] - Initial data for the form (for editing).
* @param {function} props.onSubmit - Callback function when the form is submitted.
* @param {function} props.onCancel - Callback function when the form is cancelled.
* @param {boolean} props.loading - Indicates if the form is currently loading or submitting.
* @returns {JSX.Element} The ApproverHierarchyForm component.
*/
const ApproverHierarchyForm = ({ initialData, onSubmit, onCancel, loading }) => {
const { apiService, hospital } = useAuth();
const [roles, setRoles] = useState([]);
const [values, setValues] = useState(() => ({
is_active: initialData?.is_active ?? true,
levels: initialData?.levels?.length ? initialData.levels : [emptyLevel],
}));
useEffect(() => {
const fetchRoles = async () => {
try {
const data = await apiService.getRoles(hospital.id);
setRoles(data);
} catch (err) {
console.error('Failed to load roles', err);
}
};
fetchRoles();
}, [apiService, hospital]);
const handleLevelChange = (idx, field, value) => {
setValues((prev) => {
const levels = [...prev.levels];
levels[idx] = { ...levels[idx], [field]: value };
return { ...prev, levels };
});
};
/**
* Adds a new empty level to the hierarchy.
*/
const addLevel = () => setValues((prev) => ({ ...prev, levels: [...prev.levels, emptyLevel] }));
/**
* Removes a level from the hierarchy at a specific index.
* @param {number} idx - The index of the level to remove.
*/
const removeLevel = (idx) => setValues((prev) => ({
...prev,
levels: prev.levels.filter((_, i) => i !== idx),
}));
/**
* Handles changes to the checkbox input.
* Updates the form values state.
*
* @param {ChangeEvent<HTMLInputElement>} e - The change event object.
*/
const handleCheckbox = (e) => {
const { name, checked } = e.target;
setValues((prev) => ({ ...prev, [name]: checked }));
};
const handleSubmit = (e) => {
e.preventDefault();
// Validate each level has a role
for (const lvl of values.levels) {
if (!lvl.role) {
alert('Each level must have a role selected');
return;
}
}
const payload = {
is_active: values.is_active,
// Assign priority based on the order in the array
levels: values.levels.map((lvl, idx) => ({
role: lvl.role,
priority: idx + 1,
})),
};
// Pass the structured payload to the parent component
onSubmit(payload);
return;
console.log(values);
onSubmit(values);
};
return (
<div className="max-w-4xl mx-auto p-4">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Header Section */}
<div className="bg-white rounded-lg border border-gray-200 p-4">
<Checkbox
name="is_active"
label="Hierarchy Active"
checked={values.is_active}
onChange={handleCheckbox}
/>
</div>
{/* Levels Section */}
<div className="bg-white rounded-lg border border-gray-200">
<div className="p-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h4 className="font-semibold text-gray-900">Approval Levels</h4>
<button
type="button"
onClick={addLevel}
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-md transition-colors"
>
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add Level
</button>
</div>
</div>
<div className="divide-y divide-gray-200">
{values.levels.map((lvl, idx) => (
<div key={idx} className="p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-gray-700">
Level {idx + 1}
</span>
{values.levels.length > 1 && (
<button
type="button"
onClick={() => removeLevel(idx)}
className="inline-flex items-center px-2 py-1 text-sm text-red-600 hover:text-red-700 hover:bg-red-50 rounded-md transition-colors"
>
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Remove
</button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Role Selection */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Role
</label>
<select
className="w-full border border-gray-300 rounded-md px-3 py-2 bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
value={lvl.role}
onChange={(e) => handleLevelChange(idx, 'role', e.target.value)}
>
<option value="">Select a role</option>
{roles.map((role) => (
<option key={role.id} value={role.id}>
{role.name}
</option>
))}
</select>
</div>
{/* Priority derived from order */}
<div>
<span className="text-sm text-gray-600">Priority: {idx + 1}</span>
</div>
</div>
</div>
))}
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-3 sm:justify-end">
<button
type="button"
onClick={onCancel}
className="w-full sm:w-auto px-6 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={loading}
>
Cancel
</button>
<button
type="submit"
className="w-full sm:w-auto px-6 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={loading}
>
{loading ? (
<span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Saving...
</span>
) : (
initialData ? 'Update Hierarchy' : 'Create Hierarchy'
)}
</button>
</div>
</form>
</div>
);
};
export default ApproverHierarchyForm;
Source