Skip to content

Commit

Permalink
xras ui admin React components for new interface
Browse files Browse the repository at this point in the history
  • Loading branch information
asimregmi committed Sep 24, 2024
1 parent 0a848bc commit 729c67b
Show file tree
Hide file tree
Showing 11 changed files with 5,610 additions and 538 deletions.
4,897 changes: 4,897 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

209 changes: 208 additions & 1 deletion src/edit-resource/EditResource.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,210 @@
import React, { useEffect, useReducer, useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import LoadingSpinner from '../shared/LoadingSpinner';
import FormField from '../shared/Form/FormField';
import { SelectInput } from '../shared/SelectInput/SelectInput';
import Grid from '../shared/Grid';
import Alert from '../shared/Alert';
import style from './EditResource.module.scss';
import { resources } from './helpers/reducers';
import { setLoading, setResourceData, setSuccessMessage, updateResourceField, updateAllocation } from './helpers/actions';
import { fetchResourceData, updateResourceData } from './helpers/utils';

const ResourceForm = ({ resourceDetails, resourceTypesOptions, unitTypesOptions, dispatch }) => (
<>
<FormField
label="Resource Name"
value={resourceDetails.resource_name}
onChange={(e) => dispatch(updateResourceField('resource_name', e.target.value))}
className="w-100"
/>
<FormField
label="Resource ID"
value={resourceDetails.resource_id}
disabled
/>
<FormField
label="Dollar Value"
value={resourceDetails.dollar_value}
onChange={(e) => dispatch(updateResourceField('dollar_value', e.target.value))}
/>
<FormField
label="Description"
type="textarea"
value={resourceDetails.description}
onChange={(e) => dispatch(updateResourceField('description', e.target.value))}
/>
<SelectInput
label="Resource Type"
options={resourceTypesOptions}
value={resourceDetails.resource_type_id}
onChange={(e) => dispatch(updateResourceField('resource_type_id', e.target.value))}
/>
<SelectInput
label="Unit Type"
options={unitTypesOptions}
value={resourceDetails.unit_type_id}
onChange={(e) => dispatch(updateResourceField('unit_type_id', e.target.value))}
/>
</>
);

ResourceForm.propTypes = {
resourceDetails: PropTypes.object.isRequired,
resourceTypesOptions: PropTypes.array.isRequired,
unitTypesOptions: PropTypes.array.isRequired,
dispatch: PropTypes.func.isRequired,
};

const ALLOCATION_COLUMNS = [
{ key: 'display_name', name: 'Allocation Type', width: 200 },
{ key: 'allowed_actions', name: 'Allowed Actions', width: 200, type: 'select' },
{ key: 'resource_order', name: 'Resource Order', width: 100, type: 'text' },
{ key: 'comment', name: 'Comment', width: 300, type: 'input' },
];

export default function EditResource({ resourceId }) {
return <>Edit resource ID {resourceId}!</>;
const [state, dispatch] = useReducer(resources, {
resourceData: null,
loading: true,
error: null,
successMessage: { message: '', color: '' },
});

const handleError = async (dispatch, response, defaultMessage) => {
let errorMessage = defaultMessage;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || defaultMessage;
} catch (parseError) {
console.error('Error parsing error response:', parseError);
}
dispatch(setSuccessMessage(errorMessage, 'danger'));
};

const fetchData = useCallback(async () => {
dispatch(setLoading(true));
try {
const data = await fetchResourceData(resourceId);
dispatch(setResourceData(data));
} catch (error) {
console.error('Failed to fetch resource data:', error);
dispatch({ type: 'SET_ERROR', payload: 'Failed to fetch resource data. Please try again later.' });
}
}, [resourceId]);

useEffect(() => {
fetchData();
}, [fetchData]);

const { resourceData, loading, error, successMessage } = state;

const resourceDetails = resourceData?.resource_details;

const allowedActionsOptions = useMemo(() =>
resourceData?.allowed_actions_available?.map(action => ({
value: action.resource_state_type_id,
label: action.display_resource_state_type,
})) || [],
[resourceData]);

const resourceTypesOptions = useMemo(() =>
resourceData?.resource_types_available?.map(type => ({
value: type.resource_type_id,
label: type.display_resource_type,
})) || [],
[resourceData]);

const unitTypesOptions = useMemo(() =>
resourceData?.unit_types_available?.map(type => ({
value: type.unit_type_id,
label: type.display_unit_type,
})) || [],
[resourceData]);

const allocationRows = useMemo(() =>
resourceDetails?.allocation_types?.map(type => ({
display_name: type.display_name,
allowed_actions: {
options: allowedActionsOptions,
value: type.allowed_action?.resource_state_type_id || '',
onChange: newValue => dispatch(updateAllocation(type, {
allowed_action: {
...type.allowed_action,
resource_state_type_id: newValue,
},
})),
},
resource_order: type.resource_order,
comment: {
value: type.comment || '',
onChange: newValue => dispatch(updateAllocation(type, { comment: newValue })),
},
})) || [],
[resourceDetails, allowedActionsOptions]);

const handleSubmit = useCallback(async () => {
if (!resourceDetails) return;

const updatedResource = {
resource_name: resourceDetails.resource_name,
description: resourceDetails.description,
resource_type_id: resourceDetails.resource_type_id,
unit_type_id: resourceDetails.unit_type_id,
allocation_types: resourceDetails.allocation_types.map(type => ({
allocation_type_id: type.allocation_type_id,
allowed_action: {
resource_state_type_id: type.allowed_action.resource_state_type_id,
},
comment: type.comment,
resource_order: type.resource_order,
})),
};

try {
const response = await updateResourceData(resourceId, updatedResource);
if (response.ok) {
const result = await response.json();
dispatch(setSuccessMessage('Resource updated successfully!', 'success'));
console.log(result.message);
} else {
await handleError(dispatch, response, 'Failed to update resource');
}
} catch (error) {
console.error('Error updating resource:', error);
dispatch(setSuccessMessage('Error updating resource. Please try again later.', 'danger'));
}
}, [resourceDetails, resourceId]);

if (loading) return <LoadingSpinner />;
if (error) return <Alert color="danger">{error}</Alert>;
if (!resourceData) return <div>No resource data available.</div>;

return (
<div className="edit-resource">
<h2>Edit Resource</h2>

{successMessage.message && <Alert color={successMessage.color}>{successMessage.message}</Alert>}

<ResourceForm
resourceDetails={resourceDetails}
resourceTypesOptions={resourceTypesOptions}
unitTypesOptions={unitTypesOptions}
dispatch={dispatch}
/>

<h2>Allocation Types</h2>
<Grid
classes={style["no-scroll-grid"]}
columns={ALLOCATION_COLUMNS}
rows={allocationRows}
/>

<button className="btn btn-primary" onClick={handleSubmit}>Save Resource</button>
</div>
);
}

EditResource.propTypes = {
resourceId: PropTypes.number.isRequired,
};
4 changes: 4 additions & 0 deletions src/edit-resource/EditResource.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.no-scroll-grid {
overflow: auto;
height: auto;
}
26 changes: 26 additions & 0 deletions src/edit-resource/helpers/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const setResourceData = (data) => ({
type: 'SET_RESOURCE_DATA',
payload: data,
});

export const setLoading = (loading) => ({
type: 'SET_LOADING',
payload: loading,
});

export const setSuccessMessage = (message, color) => ({
type: 'SET_SUCCESS_MESSAGE',
payload: { message, color },
});

export const updateResourceField = (field, value) => ({
type: 'UPDATE_RESOURCE_FIELD',
field,
value,
});

export const updateAllocation = (type, updates) => ({
type: 'UPDATE_ALLOCATION',
payload: { type, updates },
});

39 changes: 39 additions & 0 deletions src/edit-resource/helpers/reducers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export const resources = (state, action) => {
switch (action.type) {
case 'SET_RESOURCE_DATA':
return { ...state, resourceData: action.payload, loading: false };
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_SUCCESS_MESSAGE':
return { ...state, successMessage: action.payload };
case 'UPDATE_RESOURCE_FIELD':
return {
...state,
resourceData: {
...state.resourceData,
resource_details: {
...state.resourceData.resource_details,
[action.field]: action.value,
},
},
};
case 'UPDATE_ALLOCATION':
return {
...state,
resourceData: {
...state.resourceData,
resource_details: {
...state.resourceData.resource_details,
allocation_types: state.resourceData.resource_details.allocation_types.map(alloc =>
alloc === action.payload.type
? { ...alloc, ...action.payload.updates }
: alloc
),
},
},
};
default:
return state;
}
};

17 changes: 17 additions & 0 deletions src/edit-resource/helpers/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const fetchResourceData = async (resourceId) => {
const response = await fetch(`/resources/${resourceId}.json`);
return await response.json();
};

export const updateResourceData = async (resourceId, updatedResource) => {
const response = await fetch(`/resources/${resourceId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify({ resource: updatedResource }),
});
return response;
};

21 changes: 21 additions & 0 deletions src/shared/Checkbox/CheckboxGroup.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';

export default function CheckboxGroup({ label, options, onChange }) {
return (
<div className="form-group">
<label>{label}</label>
{options.map((option, idx) => (
<div key={idx} className="form-check">
<input
className="form-check-input"
type="checkbox"
value={option.value}
checked={option.checked}
onChange={() => onChange(idx)}
/>
<label className="form-check-label">{option.label}</label>
</div>
))}
</div>
);
};
25 changes: 25 additions & 0 deletions src/shared/Form/FormField.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';

export default function FormField({ label, type = 'text', value, onChange, disabled = false, className }) {
return (
<div className={`form-group ${className}`}>
<label>{label}</label>
{type === 'textarea' ? (
<textarea
className="form-control"
value={value}
onChange={onChange}
disabled={disabled}
></textarea>
) : (
<input
type={type}
className="form-control"
value={value}
onChange={onChange}
disabled={disabled}
/>
)}
</div>
);
}
35 changes: 33 additions & 2 deletions src/shared/Grid.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
import { useLayoutEffect, useRef } from "react";
import gridStyle from "./Grid.module.scss";

import gridStyle from './Grid.module.scss';
import { SelectInput } from '../shared/SelectInput/SelectInput';
import FormField from '../shared/Form/FormField';
import GridText from "./GridText";
import CheckboxGroup from '../shared/Checkbox/CheckboxGroup'; // Assuming you are using CheckboxGroup

const columnTypeComponents = {
text: GridText,
select: ({ column, row, style }) => (
<td style={style}>
<SelectInput
label=""
options={row[column.key].options}
value={row[column.key].value}
onChange={(e) => row[column.key].onChange(e.target.value)}
/>
</td>
),
input: ({ column, row, style }) => (
<td style={style}>
<FormField
label=""
type="text"
value={row[column.key].value}
onChange={(e) => row[column.key].onChange(e.target.value)}
/>
</td>
),
checkbox: ({ column, row, style }) => (
<td style={style}>
<input
type="checkbox"
checked={row[column.key].checked}
onChange={(e) => row[column.key].onChange(e.target.checked)}
/>
</td>
),
};

export default function Grid({
Expand Down
Loading

0 comments on commit 729c67b

Please sign in to comment.