Source

components/forms/ApproverHierarchyForm.jsx

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;