diff --git a/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/MeetingServiceImpl.java b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/MeetingServiceImpl.java index 278aadfa2e9..c1fb271ecc7 100644 --- a/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/MeetingServiceImpl.java +++ b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/MeetingServiceImpl.java @@ -55,7 +55,7 @@ public List getAllMeetingsFromSite(String siteId) { return meetingRepository.getSiteMeetings(siteId); } - public List getUserMeetings(String userId, String siteId, List groupIds) { + public List getUserMeetings(String userId, String siteId, List groupIds) { return meetingRepository.getMeetings(userId, siteId, groupIds); } diff --git a/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/MeetingsController.java b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/MeetingsController.java index bc45c3e146a..47a0cffcc59 100644 --- a/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/MeetingsController.java +++ b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/MeetingsController.java @@ -32,9 +32,7 @@ import org.sakaiproject.authz.api.SecurityService; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.meetings.api.MeetingService; -import org.sakaiproject.meetings.api.model.AttendeeType; -import org.sakaiproject.meetings.api.model.Meeting; -import org.sakaiproject.meetings.api.model.MeetingAttendee; +import org.sakaiproject.meetings.api.model.*; import org.sakaiproject.meetings.controller.data.GroupData; import org.sakaiproject.meetings.controller.data.MeetingData; import org.sakaiproject.meetings.controller.data.NotificationType; @@ -43,9 +41,7 @@ import org.sakaiproject.microsoft.api.MicrosoftCommonService; import org.sakaiproject.microsoft.api.MicrosoftSynchronizationService; import org.sakaiproject.microsoft.api.SakaiProxy; -import org.sakaiproject.microsoft.api.data.MeetingRecordingData; -import org.sakaiproject.microsoft.api.data.SakaiCalendarEvent; -import org.sakaiproject.microsoft.api.data.TeamsMeetingData; +import org.sakaiproject.microsoft.api.data.*; import org.sakaiproject.microsoft.api.exceptions.MicrosoftCredentialsException; import org.sakaiproject.site.api.Group; import org.sakaiproject.site.api.Site; @@ -53,7 +49,10 @@ import org.sakaiproject.util.ResourceLoader; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -104,7 +103,17 @@ public class MeetingsController { private static final String NOTIF_CONTENT = "notification.content"; private static final String SMTP_FROM = "smtpFrom@org.sakaiproject.email.api.EmailService"; private static final String NO_REPLY = "no-reply@"; - + private static final String MEETING_ATTENDANCE_REPORT = rb.getString("meeting.attendance_report"); + private static final String MEETING_COLUMN_NAME = rb.getString("meeting.column_name"); + private static final String MEETING_COLUMN_EMAIL = rb.getString("meeting.column_email"); + private static final String MEETING_COLUMN_ROL = rb.getString("meeting.column_role"); + private static final String MEETING_COLUMN_DURATION = rb.getString("meeting.column_duration"); + private static final String MEETING_DURATION_INTERVAL = rb.getString("meeting.interval_duration"); + private static final String MEETING_ENTRY_DATE = rb.getString("meeting.entry_date"); + private static final String MEETING_EXIT_DATE = rb.getString("meeting.exit_date"); + private static final String MEETING_DETAILS= rb.getString("meeting.details"); + + /** * Check if there's an user logged * @return @@ -593,6 +602,42 @@ public void deleteMeeting(@PathVariable String meetingId) throws MeetingsExcepti throw new MeetingsException(e.getLocalizedMessage()); } } + + @GetMapping(value = "/meeting/{meetingId}/attendanceReport", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getMeetingAttendanceReport(@PathVariable String meetingId, @RequestParam(required = false) String format) throws MeetingsException { + checkCurrentUserInMeeting(meetingId); + Meeting meeting = meetingService.getMeeting(meetingId); + String onlineMeetingId = meetingService.getMeetingProperty(meeting, ONLINE_MEETING_ID); + String organizerEmail = meetingService.getMeetingProperty(meeting, ORGANIZER_USER); + checkUpdatePermissions(meeting.getSiteId()); + microsoftCommonService.inicializeMeetingNameColumns(MEETING_ATTENDANCE_REPORT, MEETING_COLUMN_NAME, MEETING_COLUMN_EMAIL, MEETING_COLUMN_ROL, MEETING_COLUMN_DURATION, MEETING_DURATION_INTERVAL, MEETING_ENTRY_DATE, MEETING_EXIT_DATE, MEETING_DETAILS); + + try { + List attendanceRecords = microsoftCommonService.getMeetingAttendanceReport(onlineMeetingId, organizerEmail); + if ("pdf".equalsIgnoreCase(format)) { + String filename = "attendance_report.pdf"; + ContentDisposition contentDisposition = ContentDisposition.builder("attachment").filename(filename).build(); + + byte[] pdfContent = microsoftCommonService.createAttendanceReportPdf(attendanceRecords); + + return ResponseEntity.ok() + .headers(h -> h.setContentDisposition(contentDisposition)) + .contentType(MediaType.APPLICATION_PDF) + .body(pdfContent); + } else if ("csv".equalsIgnoreCase(format)) { + byte[] csvContent = microsoftCommonService.createAttendanceReportCsv(attendanceRecords); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"attendance_report.csv\"") + .contentType(MediaType.TEXT_PLAIN) + .body(csvContent); + } else { + return ResponseEntity.ok(attendanceRecords); + } + } catch (Exception e) { + log.error("Error al obtener el reporte de asistencia", e); + throw new MeetingsException(e.getLocalizedMessage()); + } + } /** * Get i18n bundle diff --git a/meetings/tool/src/main/resources/Messages.properties b/meetings/tool/src/main/resources/Messages.properties index e10ea95c0e5..1a0a5503294 100644 --- a/meetings/tool/src/main/resources/Messages.properties +++ b/meetings/tool/src/main/resources/Messages.properties @@ -1,2 +1,13 @@ notification.subject=A new meeting \u0027{0}\u0027 has been published in the site \u0027{1}\u0027 notification.content=You have been invited to participate in the meeting {0}. + +#Meetings +meeting.attendance_report=Attendance Report +meeting.column_name=Name +meeting.column_email=Email +meeting.column_role=Role +meeting.column_duration=Duration (seconds) +meeting.entry_date=Entry Date +meeting.exit_date=Exit Date +meeting.interval_duration=Interval Duration (seconds) +meeting.details=Assistance Details for \ No newline at end of file diff --git a/meetings/tool/src/main/resources/Messages_ca.properties b/meetings/tool/src/main/resources/Messages_ca.properties index 4ef31140464..e72a404e23e 100644 --- a/meetings/tool/src/main/resources/Messages_ca.properties +++ b/meetings/tool/src/main/resources/Messages_ca.properties @@ -1,2 +1,13 @@ notification.subject=S\u2019ha publicat una nova reuni\u00F3 \u0027{0}\u0027 a l\u2019espai {1} -notification.content=Ha sigut convidat a participar en la reuni\u00F3 {0}. \ No newline at end of file +notification.content=Ha sigut convidat a participar en la reuni\u00F3 {0}. + +#Meetings +meeting.attendance_report=Report d\u2019assitencia +meeting.column_name=Nom +meeting.column_email=Correu +meeting.column_role=Rol +meeting.column_duration=Duraci\u00F3 (segons) +meeting.entry_date=Data d\u2019entrada +meeting.exit_date=Data d\u2019eixida +meeting.interval_duration=Interval Duration (seconds) +meeting.details=Detalls d\u2019assist\u00e8ncia per a \ No newline at end of file diff --git a/meetings/tool/src/main/resources/Messages_es.properties b/meetings/tool/src/main/resources/Messages_es.properties index 704c8b3bdd7..9848179baf3 100644 --- a/meetings/tool/src/main/resources/Messages_es.properties +++ b/meetings/tool/src/main/resources/Messages_es.properties @@ -1,2 +1,13 @@ notification.subject=Se ha publicado una nueva reuni\u00F3n \u0027{0}\u0027 en el sitio {1} notification.content=Ha sido invitado a participar en la reuni\u00F3n {0}. + +#Meetings +meeting.attendance_report=Reporte de assistencia +meeting.column_name=Nombre +meeting.column_email=Email +meeting.column_role=Rol +meeting.column_duration=Duraci\u00f3n (segundos) +meeting.entry_date=Fecha de entrada +meeting.exit_date=Fecha de salida +meeting.interval_duration=Intervalo de Duraci\u00f3n (segundos) +meeting.details=Detalles de asistencia para \ No newline at end of file diff --git a/meetings/tool/src/main/resources/card.properties b/meetings/tool/src/main/resources/card.properties index 608174f3c08..a9bcd679e04 100644 --- a/meetings/tool/src/main/resources/card.properties +++ b/meetings/tool/src/main/resources/card.properties @@ -12,6 +12,10 @@ status_text_unknown=unknown status status_text_waiting=waiting for start edit_action=Edit delete_action=Delete +download_attendance_report_action=Download attendance report +download_report_pdf= Download in pdf +download_report_excel= Download in csv +preview_report= Preview report get_link_action=Get Link message_link_copied=Link copied to clipboard check_recordings_action=Check recordings diff --git a/meetings/tool/src/main/resources/card_ca.properties b/meetings/tool/src/main/resources/card_ca.properties index 7587b6d1d1b..e11fc43af02 100644 --- a/meetings/tool/src/main/resources/card_ca.properties +++ b/meetings/tool/src/main/resources/card_ca.properties @@ -10,6 +10,10 @@ status_text_live=en directe status_text_starts=comen\u00E7a status_text_unknown=estat descononegut status_text_waiting=esperant per a comen\u00E7ar +download_attendance_report_action=Descarregar informes d\u2019assist\u00e8ncia +download_report_pdf= Descarregar en pdf +download_report_excel= Descarregar en csv +preview_report= Previsualitzar els informes edit_action=Edita delete_action=Elimina get_link_action=Obtindre un enlla\u00E7 diff --git a/meetings/tool/src/main/resources/card_es.properties b/meetings/tool/src/main/resources/card_es.properties index 880906c30cd..f4d2bf8938c 100644 --- a/meetings/tool/src/main/resources/card_es.properties +++ b/meetings/tool/src/main/resources/card_es.properties @@ -12,6 +12,10 @@ status_text_unknown=estado desconocido status_text_waiting=esperando para comenzar edit_action=Editar delete_action=Eliminar +download_attendance_report_action=Descargar informes de assistencia +download_report_pdf= Descargar en pdf +download_report_excel= Descargar en csv +preview_report= Previsualizar los informes get_link_action=Obtener Enlace message_link_copied=Enlace copiado al portapapeles check_recordings_action=Comprobar grabaciones diff --git a/meetings/tool/src/main/resources/main.properties b/meetings/tool/src/main/resources/main.properties index 51169f7e99f..0c39cabc939 100644 --- a/meetings/tool/src/main/resources/main.properties +++ b/meetings/tool/src/main/resources/main.properties @@ -8,3 +8,4 @@ past=Past search_results=Search results search=Search for meetings today=Today +meeting_alert=For the correct functioning of the tool, it is recommended to log in to Microsoft with Udl credentials. diff --git a/meetings/tool/src/main/resources/main_ca.properties b/meetings/tool/src/main/resources/main_ca.properties index ac0786eb56f..68c4f288203 100644 --- a/meetings/tool/src/main/resources/main_ca.properties +++ b/meetings/tool/src/main/resources/main_ca.properties @@ -8,4 +8,5 @@ past=Anteriors search_results=Resultats de la cerca search=Cerca de reunions today=Avui +meeting_alert=Pel correcte funcionament de l\u2019eina es recomana iniciar sessi\u00F3 a Microsoft amb les credencials UdL. diff --git a/meetings/tool/src/main/resources/main_es.properties b/meetings/tool/src/main/resources/main_es.properties index 6a273e077eb..15fee487600 100644 --- a/meetings/tool/src/main/resources/main_es.properties +++ b/meetings/tool/src/main/resources/main_es.properties @@ -8,3 +8,4 @@ past=Anteriores search_results=Resultados de b\u00fasqueda search=B\u00fasqueda de reuniones today=Hoy +meeting_alert=Para el correcto funcionamiento de la herramienta se recomienda iniciar sesi\u00f3n a Microsoft con las credenciales Udl. \ No newline at end of file diff --git a/meetings/ui/src/main/frontend/src/components/sakai-dropdown.vue b/meetings/ui/src/main/frontend/src/components/sakai-dropdown.vue index a1106638645..98a6c52da86 100644 --- a/meetings/ui/src/main/frontend/src/components/sakai-dropdown.vue +++ b/meetings/ui/src/main/frontend/src/components/sakai-dropdown.vue @@ -22,6 +22,18 @@ /> {{ item.string }} + @@ -141,5 +153,24 @@ export default { border-bottom: none; } } + +.dropdown-submenu { + box-shadow: var(--elevation-1dp); + border: 1px solid var(--button-border-color); + background-color: var(--tool-menu-background-color); + position: absolute; + padding: 0; + left: 100%; + top: 100px; + display: none; + border-radius: 10px; + list-style: none; +} + +li:hover > .dropdown-submenu { + display: flex; + flex-direction: column; + justify-content: flex-start; +} } diff --git a/meetings/ui/src/main/frontend/src/components/sakai-meeting-card.vue b/meetings/ui/src/main/frontend/src/components/sakai-meeting-card.vue index 07c8887afe5..e9292b32571 100644 --- a/meetings/ui/src/main/frontend/src/components/sakai-meeting-card.vue +++ b/meetings/ui/src/main/frontend/src/components/sakai-meeting-card.vue @@ -302,7 +302,12 @@ export default { { "string": this.i18n.edit_action, "icon": "edit", "action": this.editMeeting, "show": this.editable }, { "string": this.i18n.get_link_action, "icon": "link", "action": this.getMeetingLink, "url": this.url, "show": this.editable && this.showJoinButton }, { "string": this.i18n.check_recordings_action, "icon": "videocamera", "action": this.checkMeetingRecordings, "show": true }, - { "string": this.i18n.delete_action, "icon": "delete", "action": this.askDeleteMeeting, "show": this.editable} + { "string": this.i18n.delete_action, "icon": "delete", "action": this.askDeleteMeeting, "show": this.editable}, + { "string": this.i18n.download_attendance_report_action, "icon": "download", "show": true, + "subMenu": [ { "string": this.i18n.download_report_pdf, "icon": "filePdf", "action": () => this.downloadAttendanceReport('pdf'), "show": false }, + { "string": this.i18n.download_report_excel, "icon": "fileCsv", "action": () => this.downloadAttendanceReport('csv'), "show": true }, + { "string": this.i18n.preview_report, "icon": "eye", "action": this.downloadAttendanceReport, "show": false } + ]} ]; }, showJoinButton() { @@ -350,6 +355,30 @@ export default { .catch((error) => console.error('Error:', error)) .then((response) => this.$emit('onDeleted', this.id)); }, + downloadAttendanceReport(format) { + fetch(`${constants.toolPlacement}/meeting/${this.id}/attendanceReport?format=${format}`, { + credentials: 'include', + method: 'GET', + cache: "no-cache", + headers: { "Content-Type": "application/json; charset=utf-8" }, + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.blob(); + }) + .then(blob => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `attendance_report.${format}`; + document.body.appendChild(a); + a.click(); + a.remove(); + }) + .catch(error => console.error('Error downloading report:', error)); + }, editMeeting() { let parameters = { id: this.id, diff --git a/meetings/ui/src/main/frontend/src/resources/icons.js b/meetings/ui/src/main/frontend/src/resources/icons.js index 4ee1d946b52..94e5c8ab685 100644 --- a/meetings/ui/src/main/frontend/src/resources/icons.js +++ b/meetings/ui/src/main/frontend/src/resources/icons.js @@ -35,7 +35,11 @@ const iconsFontawsome = { presentation: "fa-desktop", remove: "fa-trash", refresh: "fa-refresh", - spinner: "fa-spinner fa-spin" + spinner: "fa-spinner fa-spin", + download: "fa fa-download", + filePdf: "fa fa-file-pdf-o", + fileCsv: "fa fa-file", + eye: "fa fa-eye", }; const iconsBootstrap = { }; diff --git a/meetings/ui/src/main/frontend/src/views/Main.vue b/meetings/ui/src/main/frontend/src/views/Main.vue index 274854f037d..31d9d4ef539 100644 --- a/meetings/ui/src/main/frontend/src/views/Main.vue +++ b/meetings/ui/src/main/frontend/src/views/Main.vue @@ -26,6 +26,7 @@ --> +
{{ i18n.meeting_alert }}

{{ i18n.search_results }}

diff --git a/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftCommonService.java b/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftCommonService.java index dce60a55def..4b917ba6e06 100644 --- a/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftCommonService.java +++ b/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftCommonService.java @@ -22,6 +22,7 @@ import org.sakaiproject.microsoft.api.data.MicrosoftDriveItemFilter; import org.sakaiproject.microsoft.api.data.MicrosoftMembersCollection; import org.sakaiproject.microsoft.api.data.MicrosoftTeam; +import org.sakaiproject.microsoft.api.data.AttendanceRecord; import org.sakaiproject.microsoft.api.data.MicrosoftUser; import org.sakaiproject.microsoft.api.data.MicrosoftUserIdentifier; import org.sakaiproject.microsoft.api.data.SynchronizationStatus; @@ -155,7 +156,11 @@ public static enum PermissionRoles { READ, WRITE } TeamsMeetingData createOnlineMeeting(String userEmail, String subject, Instant startDate, Instant endDate, List coorganizerEmails) throws MicrosoftCredentialsException; void updateOnlineMeeting(String userEmail, String meetingId, String subject, Instant startDate, Instant endDate, List coorganizerEmails) throws MicrosoftCredentialsException; List getOnlineMeetingRecordings(String onlineMeetingId, List teamIdsList, boolean force) throws MicrosoftCredentialsException; - + List getMeetingAttendanceReport(String onlineMeetingId, String userEmail) throws MicrosoftCredentialsException; + void inicializeMeetingNameColumns(String meetingAttendanceReport, String meetingName, String meetingEmail, String meetingRole, String meetingDuration, String meetingDurationInterval, String meetingEntryDate, String meetingExitDate, String meetingsDetails); + byte[] createAttendanceReportPdf(List attendanceRecords); + byte[] createAttendanceReportCsv(List attendanceRecords); + // ---------------------------------------- ONE-DRIVE (APPLICATION) -------------------------------------------------------- List getGroupDriveItems(String groupId) throws MicrosoftCredentialsException; List getGroupDriveItems(String groupId, List channelIds) throws MicrosoftCredentialsException; diff --git a/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/AttendanceInterval.java b/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/AttendanceInterval.java new file mode 100644 index 00000000000..4667f1a54a1 --- /dev/null +++ b/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/AttendanceInterval.java @@ -0,0 +1,24 @@ +package org.sakaiproject.microsoft.api.data; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.time.Instant; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AttendanceInterval { + public String joinDateTime; + public String leaveDateTime; + public int durationInSeconds; + + public String getJoinDateTime() { return joinDateTime; } + public void setJoinDateTime(String joinDateTime) { this.joinDateTime = joinDateTime; } + + public String getLeaveDateTime() { return leaveDateTime; } + public void setLeaveDateTime(String leaveDateTime) { this.leaveDateTime = leaveDateTime; } + + public int getDurationInSeconds() { return durationInSeconds; } + public void setDurationInSeconds(int durationInSeconds) { this.durationInSeconds = durationInSeconds; } +} + diff --git a/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/AttendanceRecord.java b/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/AttendanceRecord.java new file mode 100644 index 00000000000..917e6c3df0f --- /dev/null +++ b/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/AttendanceRecord.java @@ -0,0 +1,35 @@ +package org.sakaiproject.microsoft.api.data; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AttendanceRecord { + + private String email; + public String id; + public String displayName; + private String role; + private int totalAttendanceInSeconds; + private List attendanceIntervals; + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + + public String getDisplayName() { return displayName; } + public void setDisplayName(String displayName) { this.displayName = displayName; } + + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } + + public int getTotalAttendanceInSeconds() { return totalAttendanceInSeconds; } + public void setTotalAttendanceInSeconds(int totalAttendanceInSeconds) { this.totalAttendanceInSeconds = totalAttendanceInSeconds; } + + public List getAttendanceIntervals() { return attendanceIntervals; } + public void setAttendanceIntervals(List attendanceIntervals) { this.attendanceIntervals = attendanceIntervals; } +} + diff --git a/microsoft-integration/impl/pom.xml b/microsoft-integration/impl/pom.xml index 7d08034b4f3..6ca95855a19 100644 --- a/microsoft-integration/impl/pom.xml +++ b/microsoft-integration/impl/pom.xml @@ -88,7 +88,18 @@ javax.mail mail - + + com.github.librepdf + openpdf + compile + + + org.apache.commons + commons-csv + 1.10.0 + compile + + ${basedir}/src/main/java diff --git a/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftCommonServiceImpl.java b/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftCommonServiceImpl.java index 8b6acabb3e1..a6afb880cb4 100644 --- a/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftCommonServiceImpl.java +++ b/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftCommonServiceImpl.java @@ -15,6 +15,7 @@ */ package org.sakaiproject.microsoft.impl; +import java.io.*; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; @@ -39,6 +40,13 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import com.google.gson.*; +import com.lowagie.text.*; +import com.lowagie.text.pdf.PdfPTable; +import com.lowagie.text.pdf.PdfWriter; +import com.microsoft.graph.requests.*; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; import com.microsoft.graph.content.BatchRequestContent; import com.microsoft.graph.content.BatchResponseContent; @@ -60,6 +68,7 @@ import org.sakaiproject.microsoft.api.MicrosoftAuthorizationService; import org.sakaiproject.microsoft.api.MicrosoftCommonService; import org.sakaiproject.microsoft.api.SakaiProxy; +import org.sakaiproject.microsoft.api.data.*; import org.sakaiproject.microsoft.api.data.MeetingRecordingData; import org.sakaiproject.microsoft.api.data.MicrosoftChannel; import org.sakaiproject.microsoft.api.data.MicrosoftCredentials; @@ -86,10 +95,6 @@ import org.springframework.cache.CacheManager; import org.springframework.transaction.annotation.Transactional; -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; - import com.microsoft.graph.tasks.LargeFileUploadTask; import com.microsoft.graph.tasks.LargeFileUploadResult; import com.microsoft.graph.models.MeetingChatMode; @@ -176,6 +181,17 @@ public class MicrosoftCommonServiceImpl implements MicrosoftCommonService { private static final int UDL_CODE_SIZE = 15; private static final String TEAM_CHARACTER_SEPARATOR = "..."; + private static final Font BOLD_FONT = FontFactory.getFont(FontFactory.HELVETICA, 10, Font.BOLD); + + private String MEETING_ATTENDANCE_REPORT; + private String MEETING_COLUMN_NAME; + private String MEETING_COLUMN_EMAIL; + private String MEETING_COLUMN_ROLE; + private String MEETING_COLUMN_DURATION; + private String MEETING_DURATION_INTERVAL; + private String MEETING_ENTRY_DATE; + private String MEETING_EXIT_DATE; + private String MEETING_DETAILS; @Setter private ServerConfigurationService serverConfigurationService; @Setter private CacheManager cacheManager; @@ -2300,6 +2316,157 @@ public List getOnlineMeetingRecordings(String onlineMeetin return ret; } + @Override + public List getMeetingAttendanceReport(String onlineMeetingId, String userEmail) throws MicrosoftCredentialsException { + + List attendanceRecordsResponse = new ArrayList<>(); + + MicrosoftUser organizerUser = getUserByEmail(userEmail); + + MeetingAttendanceReportCollectionPage attendanceReports = getGraphClient() + .users(organizerUser.getId()) + .onlineMeetings(onlineMeetingId) + .attendanceReports() + .buildRequest() + .get(); + + if (attendanceReports != null && attendanceReports.getCurrentPage() != null) { + for (com.microsoft.graph.models.MeetingAttendanceReport report : attendanceReports.getCurrentPage()) { + String reportId = report.id; + + AttendanceRecordCollectionPage attendanceRecords = getGraphClient() + .users(organizerUser.getId()) + .onlineMeetings() + .byId(onlineMeetingId) + .attendanceReports(reportId) + .attendanceRecords() + .buildRequest() + .get(); + + if (attendanceRecords != null && attendanceRecords.getCurrentPage() != null) { + for (com.microsoft.graph.models.AttendanceRecord record : attendanceRecords.getCurrentPage()) { + AttendanceRecord response = new AttendanceRecord(); + + if (record.emailAddress != null) response.setEmail(record.emailAddress); + if (record.id != null) response.setId(record.id); + if (record.identity.displayName != null) response.setDisplayName(record.identity.displayName); + if (record.role != null) response.setRole(record.role); + if (record.totalAttendanceInSeconds != null) response.setTotalAttendanceInSeconds(record.totalAttendanceInSeconds); + + List intervals = new ArrayList<>(); + if (record.attendanceIntervals != null) { + for (com.microsoft.graph.models.AttendanceInterval interval : record.attendanceIntervals) { + AttendanceInterval intervalResponse = new AttendanceInterval(); + if (interval.joinDateTime != null) intervalResponse.setJoinDateTime(interval.joinDateTime.toString()); + if (interval.leaveDateTime != null) intervalResponse.setLeaveDateTime(interval.leaveDateTime.toString()); + if (interval.durationInSeconds != null) intervalResponse.setDurationInSeconds(interval.durationInSeconds); + intervals.add(intervalResponse); + } + } + response.setAttendanceIntervals(intervals); + + + attendanceRecordsResponse.add(response); + + } + } + + } + + + } + return attendanceRecordsResponse; + } + + + public void inicializeMeetingNameColumns(String meetingAttendanceReport, String meetingName, String meetingEmail, String meetingRole, String meetingDuration, String meetingDurationInterval, String meetingEntryDate, String meetingExitDate, String meetingsDetails) { + MEETING_ATTENDANCE_REPORT= meetingAttendanceReport; + MEETING_COLUMN_NAME = meetingName; + MEETING_COLUMN_EMAIL = meetingEmail; + MEETING_COLUMN_ROLE = meetingRole; + MEETING_COLUMN_DURATION = meetingDuration; + MEETING_DURATION_INTERVAL = meetingDurationInterval; + MEETING_ENTRY_DATE = meetingEntryDate; + MEETING_EXIT_DATE = meetingExitDate; + MEETING_DETAILS = meetingsDetails; + } + + @Transactional(readOnly = true) + public byte[] createAttendanceReportPdf(List attendanceRecords) { + + com.lowagie.text.Document document = new Document(PageSize.A4.rotate()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PdfWriter.getInstance(document, out); + document.open(); + + Paragraph title = new Paragraph(MEETING_ATTENDANCE_REPORT, BOLD_FONT); + title.setAlignment(Element.ALIGN_CENTER); + document.add(title); + document.add(new Paragraph(" ")); + + PdfPTable table = new PdfPTable(4); + table.addCell(MEETING_COLUMN_NAME); + table.addCell(MEETING_COLUMN_EMAIL); + table.addCell(MEETING_COLUMN_ROLE); + table.addCell(MEETING_COLUMN_DURATION); + + + for (AttendanceRecord record : attendanceRecords) { + String displayName = record.getDisplayName(); + String email = record.getEmail(); + String role = record.getRole(); + String duration = String.valueOf(record.getTotalAttendanceInSeconds()); + + table.addCell(displayName); + table.addCell(email); + table.addCell(role); + table.addCell(duration); + } + + document.add(table); + + document.add(new Paragraph(" ")); + + for (AttendanceRecord record : attendanceRecords) { + for (AttendanceInterval interval : record.getAttendanceIntervals()) { + document.add(new Paragraph(MEETING_DETAILS + " " + record.getDisplayName() + ":")); + document.add(new Paragraph("- " + MEETING_ENTRY_DATE + interval.getJoinDateTime())); + document.add(new Paragraph("- " + MEETING_EXIT_DATE + interval.getLeaveDateTime())); + document.add(new Paragraph("- " + MEETING_DURATION_INTERVAL + interval.getDurationInSeconds())); + document.add(new Paragraph(" ")); + } + } + + document.close(); + return out.toByteArray(); + } + + @Transactional(readOnly = true) + public byte[] createAttendanceReportCsv(List attendanceRecords) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (CSVPrinter csvPrinter = new CSVPrinter(new PrintWriter(out), CSVFormat.DEFAULT + .withHeader(MEETING_COLUMN_NAME, MEETING_COLUMN_EMAIL, MEETING_COLUMN_ROLE, MEETING_COLUMN_DURATION, MEETING_ENTRY_DATE, MEETING_EXIT_DATE, MEETING_DURATION_INTERVAL))) { + + for (AttendanceRecord record : attendanceRecords) { + for (AttendanceInterval interval : record.getAttendanceIntervals()) { + csvPrinter.printRecord( + record.getDisplayName(), + record.getEmail(), + record.getRole(), + record.getTotalAttendanceInSeconds(), + interval.getJoinDateTime(), + interval.getLeaveDateTime(), + interval.getDurationInSeconds() + ); + } + } + } catch (IOException e) { + log.error("Error creating CSV file", e); + } + + return out.toByteArray(); + } + // ---------------------------------------- ONE-DRIVE (APPLICATION) -------------------------------------------------------- @Override public List getGroupDriveItems(String groupId) throws MicrosoftCredentialsException {