feat(ratecard-table): improve rate handling and focus management in the ratecard table
This commit is contained in:
@@ -57,14 +57,14 @@ export default class ProjectRateCardController extends WorklenzControllerBase {
|
|||||||
fprr.*,
|
fprr.*,
|
||||||
jt.name as jobtitle,
|
jt.name as jobtitle,
|
||||||
(
|
(
|
||||||
SELECT COALESCE(json_agg(pm.id), '[]'::json)
|
SELECT COALESCE(json_agg(pm.id), '[]'::json)
|
||||||
FROM project_members pm
|
FROM project_members pm
|
||||||
WHERE pm.project_rate_card_role_id = fprr.id
|
WHERE pm.project_rate_card_role_id = fprr.id
|
||||||
) AS members
|
) AS members
|
||||||
FROM finance_project_rate_card_roles fprr
|
FROM finance_project_rate_card_roles fprr
|
||||||
LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id
|
LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id
|
||||||
WHERE fprr.project_id = $1
|
WHERE fprr.project_id = $1
|
||||||
ORDER BY jt.name;
|
ORDER BY fprr.created_at;
|
||||||
`;
|
`;
|
||||||
const result = await db.query(q, [project_id]);
|
const result = await db.query(q, [project_id]);
|
||||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||||
|
|||||||
@@ -30,13 +30,14 @@ const RatecardTable: React.FC = () => {
|
|||||||
const rolesRedux = useAppSelector((state) => state.projectFinanceRateCard.rateCardRoles) || [];
|
const rolesRedux = useAppSelector((state) => state.projectFinanceRateCard.rateCardRoles) || [];
|
||||||
const isLoading = useAppSelector((state) => state.projectFinanceRateCard.isLoading);
|
const isLoading = useAppSelector((state) => state.projectFinanceRateCard.isLoading);
|
||||||
const currency = useAppSelector((state) => state.financeReducer.currency).toUpperCase();
|
const currency = useAppSelector((state) => state.financeReducer.currency).toUpperCase();
|
||||||
|
const rateInputRefs = React.useRef<Array<HTMLInputElement | null>>([]);
|
||||||
// Local state for editing
|
// Local state for editing
|
||||||
const [roles, setRoles] = useState<JobRoleType[]>(rolesRedux);
|
const [roles, setRoles] = useState<JobRoleType[]>(rolesRedux);
|
||||||
const [addingRow, setAddingRow] = useState<boolean>(false);
|
const [addingRow, setAddingRow] = useState<boolean>(false);
|
||||||
const [jobTitles, setJobTitles] = useState<RatecardType[]>([]);
|
const [jobTitles, setJobTitles] = useState<RatecardType[]>([]);
|
||||||
const [members, setMembers] = useState<IProjectMemberViewModel[]>([]);
|
const [members, setMembers] = useState<IProjectMemberViewModel[]>([]);
|
||||||
const [isLoadingMembers, setIsLoading] = useState(false);
|
const [isLoadingMembers, setIsLoading] = useState(false);
|
||||||
|
const [focusRateIndex, setFocusRateIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
const pagination = {
|
const pagination = {
|
||||||
current: 1,
|
current: 1,
|
||||||
@@ -91,6 +92,13 @@ const RatecardTable: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [dispatch, projectId]);
|
}, [dispatch, projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (focusRateIndex !== null && rateInputRefs.current[focusRateIndex]) {
|
||||||
|
rateInputRefs.current[focusRateIndex]?.focus();
|
||||||
|
setFocusRateIndex(null);
|
||||||
|
}
|
||||||
|
}, [roles, focusRateIndex]);
|
||||||
|
|
||||||
// Add new role row
|
// Add new role row
|
||||||
const handleAddRole = () => {
|
const handleAddRole = () => {
|
||||||
setAddingRow(true);
|
setAddingRow(true);
|
||||||
@@ -110,7 +118,7 @@ const RatecardTable: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle job title select for new row
|
// In handleSelectJobTitle, after successful insert, update the rate if needed
|
||||||
const handleSelectJobTitle = async (jobTitleId: string) => {
|
const handleSelectJobTitle = async (jobTitleId: string) => {
|
||||||
const jobTitle = jobTitles.find((jt) => jt.id === jobTitleId);
|
const jobTitle = jobTitles.find((jt) => jt.id === jobTitleId);
|
||||||
if (!jobTitle || !projectId) return;
|
if (!jobTitle || !projectId) return;
|
||||||
@@ -120,27 +128,21 @@ const RatecardTable: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (insertProjectRateCardRole.fulfilled.match(resultAction)) {
|
if (insertProjectRateCardRole.fulfilled.match(resultAction)) {
|
||||||
const newRole = resultAction.payload;
|
// Re-fetch roles and focus the last one (newly added)
|
||||||
setRoles([
|
dispatch(fetchProjectRateCardRoles(projectId)).then(() => {
|
||||||
...roles,
|
setFocusRateIndex(roles.length); // The new row will be at the end
|
||||||
{
|
});
|
||||||
id: newRole.id,
|
|
||||||
job_title_id: newRole.job_title_id,
|
|
||||||
jobtitle: newRole.jobtitle,
|
|
||||||
rate: newRole.rate,
|
|
||||||
members: [], // Initialize members array
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
setAddingRow(false);
|
setAddingRow(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle rate change
|
// Update handleRateChange to always update local state
|
||||||
const handleRateChange = (value: string | number, index: number) => {
|
const handleRateChange = (value: string | number, index: number) => {
|
||||||
const updatedRoles = roles.map((role, idx) =>
|
setRoles(prev =>
|
||||||
idx === index ? { ...role, rate: Number(value) } : role
|
prev.map((role, idx) =>
|
||||||
|
idx === index ? { ...role, rate: Number(value) } : role
|
||||||
|
)
|
||||||
);
|
);
|
||||||
setRoles(updatedRoles);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle delete
|
// Handle delete
|
||||||
@@ -176,6 +178,18 @@ const RatecardTable: React.FC = () => {
|
|||||||
console.error('Error assigning member:', error);
|
console.error('Error assigning member:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Separate function for updating rate if changed
|
||||||
|
const handleRateBlur = (value: string, index: number) => {
|
||||||
|
if (value !== roles[index].rate) {
|
||||||
|
dispatch(updateProjectRateCardRoleById({
|
||||||
|
id: roles[index].id!,
|
||||||
|
body: {
|
||||||
|
job_title_id: roles[index].job_title_id,
|
||||||
|
rate: value,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Columns
|
// Columns
|
||||||
const columns: TableProps<JobRoleType>['columns'] = [
|
const columns: TableProps<JobRoleType>['columns'] = [
|
||||||
@@ -216,6 +230,7 @@ const RatecardTable: React.FC = () => {
|
|||||||
align: 'right',
|
align: 'right',
|
||||||
render: (value: number, record: JobRoleType, index: number) => (
|
render: (value: number, record: JobRoleType, index: number) => (
|
||||||
<Input
|
<Input
|
||||||
|
ref={el => rateInputRefs.current[index] = el}
|
||||||
type="number"
|
type="number"
|
||||||
value={roles[index]?.rate ?? 0}
|
value={roles[index]?.rate ?? 0}
|
||||||
min={0}
|
min={0}
|
||||||
@@ -228,17 +243,8 @@ const RatecardTable: React.FC = () => {
|
|||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
}}
|
}}
|
||||||
onChange={(e) => handleRateChange(e.target.value, index)}
|
onChange={(e) => handleRateChange(e.target.value, index)}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => handleRateBlur(e.target.value, index)}
|
||||||
if (e.target.value !== roles[index].rate) {
|
onPressEnter={(e) => handleRateBlur(e.target.value, index)}
|
||||||
dispatch(updateProjectRateCardRoleById({
|
|
||||||
id: roles[index].id!,
|
|
||||||
body: {
|
|
||||||
job_title_id: roles[index].job_title_id,
|
|
||||||
rate: e.target.value,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user