Skip to content

Commit eb733fc

Browse files
committed
chore: show user invitations
1 parent c528c45 commit eb733fc

23 files changed

Lines changed: 194 additions & 8 deletions

File tree

api/src/main/java/com/grash/controller/UserController.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.grash.advancedsearch.SearchCriteria;
55
import com.grash.dto.*;
66
import com.grash.exception.CustomException;
7+
import com.grash.service.UserInvitationService;
78
import com.grash.mapper.UserMapper;
89
import com.grash.model.User;
910
import com.grash.model.Role;
@@ -44,6 +45,7 @@ public class UserController {
4445
private final UserMapper userMapper;
4546
private final IntercomService intercomService;
4647
private final CompanyService companyService;
48+
private final UserInvitationService userInvitationService;
4749

4850
@PostMapping("/search")
4951
@PreAuthorize("permitAll()")
@@ -100,6 +102,14 @@ public SuccessResponse invite(@Parameter(description = "User invitation data") @
100102
} else throw new CustomException("Access denied", HttpStatus.FORBIDDEN);
101103
}
102104

105+
@GetMapping("/invitations/last-week")
106+
@PreAuthorize("hasRole('ROLE_CLIENT')")
107+
public Collection<UserInvitationMiniDTO> getLastWeekInvitations(@Parameter(hidden = true) @CurrentUser User user) {
108+
if (user.getRole().getViewPermissions().contains(PermissionEntity.SETTINGS)) {
109+
return userInvitationService.getDistinctByCompanyInLastWeek(user.getCompany().getId());
110+
} else throw new CustomException("Access Denied", HttpStatus.FORBIDDEN);
111+
}
112+
103113
@GetMapping("/mini")
104114
@PreAuthorize("hasRole('ROLE_CLIENT')")
105115

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.grash.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
@Data
9+
@NoArgsConstructor
10+
@AllArgsConstructor
11+
public class UserInvitationMiniDTO {
12+
@Schema(description = "Email address of the invited user")
13+
private String email;
14+
15+
@Schema(description = "Role ID assigned to the invitation")
16+
private Long roleId;
17+
18+
@Schema(description = "Role name assigned to the invitation")
19+
private String roleName;
20+
}

api/src/main/java/com/grash/repository/UserInvitationRepository.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,20 @@
22

33
import com.grash.model.UserInvitation;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Query;
6+
import org.springframework.data.repository.query.Param;
57

6-
import java.util.Collection;
8+
import java.util.Date;
79
import java.util.List;
810

911
public interface UserInvitationRepository extends JpaRepository<UserInvitation, Long> {
1012
List<UserInvitation> findByRole_IdAndEmailIgnoreCase(Long id, String email);
13+
14+
@Query("SELECT u FROM UserInvitation u WHERE u.createdBy IN " +
15+
"(SELECT us.id FROM User us WHERE us.company.id = :companyId) " +
16+
"AND u.createdAt >= :since " +
17+
"AND NOT EXISTS (SELECT u2 FROM User u2 WHERE lower(u2.email) = lower(u.email))")
18+
List<UserInvitation> findPendingByCompanyAndCreatedAtAfter(@Param("companyId") Long companyId,
19+
@Param("since") Date since);
1120
}
1221

api/src/main/java/com/grash/service/UserInvitationService.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package com.grash.service;
22

3+
import com.grash.dto.UserInvitationMiniDTO;
34
import com.grash.model.UserInvitation;
45
import com.grash.repository.UserInvitationRepository;
56
import lombok.RequiredArgsConstructor;
67
import org.springframework.stereotype.Service;
78

8-
import java.util.Collection;
9-
import java.util.List;
10-
import java.util.Optional;
9+
import java.util.*;
10+
import java.util.function.Function;
11+
import java.util.stream.Collectors;
1112

1213
@Service
1314
@RequiredArgsConstructor
@@ -34,4 +35,23 @@ public List<UserInvitation> findByRoleAndEmail(Long id, String email) {
3435
return userInvitationRepository.findByRole_IdAndEmailIgnoreCase(id, email);
3536
}
3637

38+
public Collection<UserInvitationMiniDTO> getDistinctByCompanyInLastWeek(Long companyId) {
39+
Calendar cal = Calendar.getInstance();
40+
cal.add(Calendar.DAY_OF_YEAR, -7);
41+
Date weekAgo = cal.getTime();
42+
43+
List<UserInvitation> invitations = userInvitationRepository.findPendingByCompanyAndCreatedAtAfter(companyId,
44+
weekAgo);
45+
46+
return invitations.stream()
47+
.collect(Collectors.toMap(
48+
inv -> inv.getEmail().toLowerCase(),
49+
Function.identity(),
50+
(inv1, inv2) -> inv1.getCreatedAt().after(inv2.getCreatedAt()) ? inv1 : inv2
51+
))
52+
.values().stream()
53+
.map(inv -> new UserInvitationMiniDTO(inv.getEmail(), inv.getRole().getId(), inv.getRole().getName()))
54+
.collect(Collectors.toList());
55+
}
56+
3757
}

frontend/src/content/own/PeopleAndTeams/People.tsx

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
Box,
3+
Button,
34
debounce,
45
Dialog,
56
DialogContent,
@@ -27,8 +28,10 @@ import {
2728
disableUser,
2829
editUser,
2930
editUserRole,
31+
getLastWeekInvitations,
3032
getSingleUser,
31-
getUsers
33+
getUsers,
34+
inviteUsers
3235
} from '../../../slices/user';
3336
import { OwnUser } from '../../../models/user';
3437
import { PermissionEntity, Role } from '../../../models/owns/role';
@@ -74,7 +77,9 @@ const People = ({ openModal, handleCloseModal, initialEmail }: PropsType) => {
7477
const { peopleId } = useParams();
7578
const { hasEditPermission, user } = useAuth();
7679
const [enabledOnly, setEnabledOnly] = useState<boolean>(true);
77-
const { users, loadingGet, singleUser } = useSelector((state) => state.users);
80+
const { users, loadingGet, singleUser, lastWeekInvitations } = useSelector(
81+
(state) => state.users
82+
);
7883
const [openDrawerFromUrl, setOpenDrawerFromUrl] = useState<boolean>(false);
7984
const [criteria, setCriteria] = useState<SearchCriteria>({
8085
filterFields: [],
@@ -167,6 +172,14 @@ const People = ({ openModal, handleCloseModal, initialEmail }: PropsType) => {
167172
window.history.replaceState(null, 'User', `/app/people-teams/people`);
168173
setDetailDrawerOpen(false);
169174
};
175+
176+
const onResendInvites = () => {
177+
lastWeekInvitations.forEach((invitation) => {
178+
dispatch(inviteUsers(invitation.roleId, [invitation.email], false, true));
179+
});
180+
showSnackBar(t('users_invite_success'), 'success');
181+
};
182+
170183
const defautfields: Array<IField> = [
171184
{
172185
name: 'rate',
@@ -278,6 +291,10 @@ const People = ({ openModal, handleCloseModal, initialEmail }: PropsType) => {
278291
dispatch(getUsers(criteria, enabledOnly));
279292
}, [criteria, enabledOnly]);
280293

294+
useEffect(() => {
295+
dispatch(getLastWeekInvitations());
296+
}, []);
297+
281298
//see changes in ui on edit
282299
useEffect(() => {
283300
if (singleUser || users.content.length) {
@@ -444,6 +461,39 @@ const People = ({ openModal, handleCloseModal, initialEmail }: PropsType) => {
444461
/>
445462
</Stack>
446463
</Stack>
464+
{lastWeekInvitations.length > 0 && (
465+
<Box
466+
sx={{
467+
mb: 2,
468+
p: 2,
469+
bgcolor: 'info.light',
470+
color: 'info.contrastText',
471+
borderRadius: 1
472+
}}
473+
>
474+
<Stack
475+
direction="row"
476+
justifyContent="space-between"
477+
alignItems="center"
478+
>
479+
<Typography variant="subtitle2">
480+
{lastWeekInvitations.length === 1
481+
? `${lastWeekInvitations[0].email} - ${lastWeekInvitations[0].roleName}`
482+
: t('n_pending_invites', {
483+
count: lastWeekInvitations.length
484+
})}
485+
</Typography>
486+
<Button
487+
onClick={onResendInvites}
488+
variant="contained"
489+
color="secondary"
490+
size="small"
491+
>
492+
{t('resend_invites')}
493+
</Button>
494+
</Stack>
495+
</Box>
496+
)}
447497
{RenderPeopleList()}
448498

449499
<Drawer

frontend/src/i18n/translations/ar.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,8 @@ const locale = {
713713
please_type_emails: 'الرجاء كتابة رسائل البريد الإلكتروني للدعوة',
714714
please_select_role: 'الرجاء اختيار الدور',
715715
invite: 'يدعو',
716+
n_pending_invites: '{{count}} دعوات معلقة',
717+
resend_invites: 'إعادة إرسال الدعوات',
716718
team_create_success: 'تم إنشاء الفريق بنجاح',
717719
team_create_failure: 'لم يتمكن من إنشاء الفريق',
718720
team_edit_failure: 'لم يتمكن الفريق من التعديل',

frontend/src/i18n/translations/ba.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,8 @@ const locale = {
745745
please_type_emails: 'Molimo unesite e-poštu za poziv',
746746
please_select_role: 'Molimo odaberite ulogu',
747747
invite: 'Pozovi',
748+
n_pending_invites: '{{count}} pozivnica na čekanju',
749+
resend_invites: 'Ponovo pošalji pozivnice',
748750
team_create_success: 'Tim je uspješno kreiran',
749751
team_create_failure: 'Tim nije mogao biti kreiran',
750752
team_edit_failure: 'Tim nije mogao biti uređen',

frontend/src/i18n/translations/de.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,8 @@ const deJSON = {
796796
please_type_emails: 'Bitte geben Sie E-Mails ein, um einzuladen',
797797
please_select_role: 'Bitte wählen Sie eine Rolle aus',
798798
invite: 'Einladen',
799+
n_pending_invites: '{{count}} ausstehende Einladungen',
800+
resend_invites: 'Einladungen erneut senden',
799801
team_create_success: 'Das Team wurde erfolgreich erstellt',
800802
team_create_failure: 'Das Team konnte nicht erstellt werden',
801803
team_edit_failure: 'Das Team konnte nicht bearbeitet werden',

frontend/src/i18n/translations/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,8 @@ const locale = {
734734
please_type_emails: 'Please type in emails to invite',
735735
please_select_role: 'Please select a role',
736736
invite: 'Invite',
737+
n_pending_invites: '{{count}} pending invitations',
738+
resend_invites: 'Resend invitations',
737739
team_create_success: 'The Team has been created successfully',
738740
team_create_failure: "The Team couldn't be created",
739741
team_edit_failure: "The Team couldn't be edited",

frontend/src/i18n/translations/es.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,8 @@ const esJSON = {
770770
please_type_emails: 'Por favor escriba los correos electrónicos para invitar',
771771
please_select_role: 'Por favor seleccione un rol',
772772
invite: 'Invitar',
773+
n_pending_invites: '{{count}} invitaciones pendientes',
774+
resend_invites: 'Reenviar invitaciones',
773775
team_create_success: 'El equipo ha sido creado exitosamente',
774776
team_create_failure: 'El equipo no pudo ser creado',
775777
team_edit_failure: 'El equipo no pudo ser editado',

0 commit comments

Comments
 (0)