Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat partner link #109

Merged
merged 8 commits into from
Jul 25, 2024
43 changes: 21 additions & 22 deletions backend/api/projects/partnerships.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,19 @@
from backend.models.postgis.utils import timestamp


def check_if_manager(partnership_dto: ProjectPartnershipDTO):
if not ProjectAdminService.is_user_action_permitted_on_project(
token_auth.current_user(), partnership_dto.project_id
):
return {
"Error": "User is not a manager of the project",
"SubCode": "UserPermissionError",
}, 401


class ProjectPartnershipsRestApi(Resource):
def get(self, partnership_id: int):
@staticmethod
def get(partnership_id: int):
"""
Retrieves a Partnership by id
---
Expand Down Expand Up @@ -98,13 +109,9 @@ def post(self):
"""
partnership_dto = ProjectPartnershipDTO(request.get_json())

if not ProjectAdminService.is_user_action_permitted_on_project(
token_auth.current_user(), partnership_dto.project_id
):
return {
"Error": "User is not a manager of the project",
"SubCode": "UserPermissionError",
}, 401
is_not_manager_error = check_if_manager(partnership_dto)
if is_not_manager_error is not None:
return is_not_manager_error

if partnership_dto.started_on is None:
partnership_dto.started_on = timestamp()
Expand Down Expand Up @@ -183,13 +190,9 @@ def patch(partnership_id: int):
partnership_id
)

if not ProjectAdminService.is_user_action_permitted_on_project(
token_auth.current_user(), partnership_dto.project_id
):
return {
"Error": "User is not a manager of the project",
"SubCode": "UserPermissionError",
}, 401
is_not_manager_error = check_if_manager(partnership_dto)
if is_not_manager_error is not None:
return is_not_manager_error

partnership = ProjectPartnershipService.update_partnership_time_range(
partnership_id,
Expand Down Expand Up @@ -248,13 +251,9 @@ def delete(partnership_id: int):
partnership_id
)

if not ProjectAdminService.is_user_action_permitted_on_project(
token_auth.current_user(), partnership_dto.project_id
):
return {
"Error": "User is not a manager of the project",
"SubCode": "UserPermissionError",
}, 401
is_not_manager_error = check_if_manager(partnership_dto)
if is_not_manager_error is not None:
return is_not_manager_error

ProjectPartnershipService.delete_partnership(partnership_id)
return (
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/api/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,18 @@ export const useAvailableCountriesQuery = () => {
});
};

export const useAllPartnersQuery = (token, userId) => {
const fetchAllPartners = () => {
return api(token).get('partners/');
};

return useQuery({
queryKey: ['all-partners', userId],
queryFn: fetchAllPartners,
select: (response) => response.data,
});
};

const backendToQueryConversion = {
difficulty: 'difficulty',
campaign: 'campaign',
Expand Down
55 changes: 32 additions & 23 deletions frontend/src/components/projectEdit/partnersForm.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { useEffect, useState, forwardRef, useMemo } from 'react';
import { useParams } from 'react-router-dom';

import { useSelector } from 'react-redux';
import Select from 'react-select';
import ReactDatePicker from 'react-datepicker';
import { FormattedMessage } from 'react-intl';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { format } from 'date-fns';
import toast from 'react-hot-toast';
import PropTypes from 'prop-types';

import messages from './messages';
import { Alert } from '../alert';
import { ChevronDownIcon, CloseIcon } from '../svgIcons';
import { Button } from '../button';
import { styleClasses } from '../../views/projectEdit';
import { fetchLocalJSONAPI, pushToLocalJSONAPI } from '../../network/genericJSONRequest';
import { pushToLocalJSONAPI } from '../../network/genericJSONRequest';
import { useAllPartnersQuery } from '../../api/projects';
import { Listing } from './partnersListing';

export const DateCustomInput = forwardRef(
Expand All @@ -36,26 +37,39 @@ export const DateCustomInput = forwardRef(
}}
</FormattedMessage>

{(date && hideCloseIcon) || !date ? (
<div className="absolute right-1 pointer" style={{ top: '0.9rem' }} onClick={onClick}>
{((date && hideCloseIcon) || !date) && (
<button
className="absolute pointer b--none bg-inherit"
style={{ top: '0.75rem', right: '0.25rem' }}
onClick={onClick}
>
<ChevronDownIcon style={{ color: 'grey', width: '12px', height: '12px' }} />
</div>
) : null}
</button>
)}

{date && !hideCloseIcon ? (
<div
className="absolute right-1 pointer"
style={{ top: '0.75rem' }}
{date && !hideCloseIcon && (
<button
className="absolute pointer b--none bg-inherit"
style={{ top: '0.7rem', right: '0.25rem' }}
onClick={handleClear}
>
<CloseIcon style={{ color: 'grey', width: '10px', height: '10px' }} />
</div>
) : null}
</button>
)}
</div>
);
},
);

DateCustomInput.propTypes = {
value: PropTypes.string,
date: PropTypes.instanceOf(Date),
onClick: PropTypes.func.isRequired,
handleClear: PropTypes.func,
isStartDate: PropTypes.bool,
hideCloseIcon: PropTypes.bool,
};

export const PartnersForm = () => {
const [selectedPartner, setSelectedPartner] = useState({});
const [dateRange, setDateRange] = useState({
Expand All @@ -68,6 +82,7 @@ export const PartnersForm = () => {
const queryClient = useQueryClient();
const { id } = useParams();

// clear partnerNotSelectedError message when partner gets selected
useEffect(() => {
if (
selectedPartner &&
Expand All @@ -79,31 +94,25 @@ export const PartnersForm = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedPartner]);

// clear dateRange error messages when the right dates are picked
useEffect(() => {
if (!dateRange.endDate && errorMessage.id === messages.partnerEndDateError.id) {
setErrorMessage({});
return;
}

// clear error message if present when the selected endDate is after startDate
if (
dateRange.endDate &&
dateRange.startDate < dateRange.endDate &&
errorMessage.id === messages.partnerEndDateError.id
) {
setErrorMessage({});
return;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dateRange]);

const {
isPending,
isError,
data: partners,
} = useQuery({
queryKey: ['all-partners', userDetails.id],
queryFn: () => fetchLocalJSONAPI('partners/', token),
});
const { isPending, isError, data: partners } = useAllPartnersQuery(token, userDetails.id);

const savePartnerMutation = useMutation({
mutationFn: () => {
Expand Down Expand Up @@ -146,7 +155,7 @@ export const PartnersForm = () => {
}, [partners]);

const handleSave = () => {
if (!selectedPartner || !selectedPartner.id) {
if (!selectedPartner?.id) {
setErrorMessage(messages.partnerNotSelectedError);
return;
}
Expand Down
38 changes: 20 additions & 18 deletions frontend/src/components/projectEdit/partnersListing.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';

import { useSelector } from 'react-redux';
import ReactDatePicker from 'react-datepicker';
import { FormattedMessage } from 'react-intl';
Expand Down Expand Up @@ -57,11 +56,12 @@ export const Listing = ({ partnerIdToDetailsMapping }) => {
const queryClient = useQueryClient();

useEffect(() => {
if (actionType.length === 0) {
if (!actionType.length) {
setSelectedPartner({});
}
}, [actionType]);

// clear dateRange error messages when the right dates are picked
useEffect(() => {
const startDate = selectedPartner.startedOn && new Date(selectedPartner.startedOn);
const endDate = selectedPartner.endedOn && new Date(selectedPartner.endedOn);
Expand All @@ -71,9 +71,9 @@ export const Listing = ({ partnerIdToDetailsMapping }) => {
return;
}

// clear error message if present when the selected endDate is after startDate
if (endDate && startDate < endDate && errorMessage.id === messages.partnerEndDateError.id) {
setErrorMessage({});
return;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedPartner.startedOn, selectedPartner.endedOn]);
Expand All @@ -90,8 +90,7 @@ export const Listing = ({ partnerIdToDetailsMapping }) => {
const sortedPartnershipsByStartDate = response.partnerships.sort((itemA, itemB) => {
const dateA = new Date(itemA.startedOn);
const dateB = new Date(itemB.startedOn);
// return dateA - dateB; // Ascending order
return dateB - dateA; // Descending order
return dateB - dateA; // Descending order; Use dateA - dateB for ascending
});

return { partnerships: sortedPartnershipsByStartDate };
Expand Down Expand Up @@ -125,7 +124,7 @@ export const Listing = ({ partnerIdToDetailsMapping }) => {
: null;

return pushToLocalJSONAPI(
`projects/partnerships/${selectedPartner.id}`,
`projects/partnerships/${selectedPartner.id}/`,
JSON.stringify({
endedOn: endDate,
startedOn: startDate,
Expand Down Expand Up @@ -156,23 +155,26 @@ export const Listing = ({ partnerIdToDetailsMapping }) => {
updatePartnerMutation.mutate();
};

const getDateObjectAndDateString = (date) => {
const [year, month, day] = date.split('T')[0].split('-');
const dateString = `${day}/${month}/${year}`;
const dateObject = new Date(year, month - 1, day);
return [dateObject, dateString];
};

const isEmpty =
!isPending && !isRefetching && !isError && linkedPartners?.partnerships?.length === 0;

const tableContents = linkedPartners?.partnerships?.map((partner) => {
const [startYear, startMonth, startDay] = partner.startedOn.split('T')[0].split('-');
const startDateString = `${startDay}/${startMonth}/${startYear}`;
const startDate = new Date(startYear, startMonth - 1, startDay);
const [startDate, startDateString] = getDateObjectAndDateString(partner.startedOn)

let endDateString = 'N/A',
endDate = null;
if (partner.endedOn) {
const [endYear, endMonth, endDay] = partner.endedOn.split('T')[0].split('-');
endDateString = `${endDay}/${endMonth}/${endYear}`;
endDate = new Date(endYear, endMonth - 1, endDay);
[endDate, endDateString] = getDateObjectAndDateString(partner.endedOn)
}

const isInactive = endDate && endDate < new Date() ? true : false;
const isInactive = endDate && endDate < new Date();

return (
<tr
Expand Down Expand Up @@ -212,7 +214,7 @@ export const Listing = ({ partnerIdToDetailsMapping }) => {
return (
<div>
<div className="overflow-auto">
<table className="f6 w-100 mw8 center" cellspacing="0">
<table className="f6 w-100 mw8 center" cellSpacing="0">
<thead>
<tr>
<th className="fw6 f5 bb b--black-50 tl pb3 pr3">
Expand Down Expand Up @@ -274,9 +276,9 @@ export const Listing = ({ partnerIdToDetailsMapping }) => {
<FormattedMessage {...messages.partnerRemoveModalText} />
</p>

<dl class="lh-title mt0">
<dt class="f5 b">{partnerIdToDetailsMapping[selectedPartner.partnerId]?.name}</dt>
<dd class="ml0">
<dl className="lh-title mt0">
<dt className="f5 b">{partnerIdToDetailsMapping[selectedPartner.partnerId]?.name}</dt>
<dd className="ml0">
From&nbsp;
{selectedPartner.startedOn
? format(new Date(selectedPartner.startedOn), 'dd/MM/yyyy')
Expand Down Expand Up @@ -327,7 +329,7 @@ export const Listing = ({ partnerIdToDetailsMapping }) => {
<FormattedMessage {...messages.partnerUpdateModalTitle} />
</h3>
<div className="mt4 mb3 relative">
<h6 class="f5 b mb3 mt0">
<h6 className="f5 b mb3 mt0">
{partnerIdToDetailsMapping[selectedPartner.partnerId]?.name}
</h6>
<div className="flex items-center flex-wrap" style={{ gap: '1.75rem' }}>
Expand Down
Loading