diff --git a/EnotaryoPH/EnotaryoPH.Data/Constants/NotaryoEvent.cs b/EnotaryoPH/EnotaryoPH.Data/Constants/NotaryoEvent.cs new file mode 100644 index 0000000..45817f4 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Data/Constants/NotaryoEvent.cs @@ -0,0 +1,37 @@ +namespace EnotaryoPH.Data.Constants +{ + public enum NotaryoEvent + { + Unknown = 0, + PrincipalRegistered = 1, + SignatoryRegistered = 2, + WitnessRegistered = 3, + LawyerRegistered = 4, + LawyerFingerprintScanned = 5, + + IdentificationDocumentUploaded = 10, + + SelfiePassed = 15, + SelfieFailed = 16, + + DocumentUploaded = 20, + + LawyerSelected = 30, + + TransactionSubmitted = 40, + TransactionApproved = 41, + TransactionRejected = 42, + + VideoConferenceStarted = 50, + VideoRecordingStarted = 51, + VideoRecordingStopped = 52, + + SignatoryApproved = 61, + WitnessApproved = 62, + + PaymentReceived = 70, + PaymentFailed = 71, + + TransactionCompleted = 100 + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Data/Constants/SignatoryStatus.cs b/EnotaryoPH/EnotaryoPH.Data/Constants/SignatoryStatus.cs index 0897dbb..50cc511 100644 --- a/EnotaryoPH/EnotaryoPH.Data/Constants/SignatoryStatus.cs +++ b/EnotaryoPH/EnotaryoPH.Data/Constants/SignatoryStatus.cs @@ -6,6 +6,7 @@ EmailSent = 1, Registered = 2, FaceMatch = 3, - Completed = 10 + Approved = 4, + Rejected = 5, } } \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Data/Constants/TransactionState.cs b/EnotaryoPH/EnotaryoPH.Data/Constants/TransactionState.cs index 9ed50e8..20fc20a 100644 --- a/EnotaryoPH/EnotaryoPH.Data/Constants/TransactionState.cs +++ b/EnotaryoPH/EnotaryoPH.Data/Constants/TransactionState.cs @@ -7,6 +7,8 @@ DocumentUploaded = 2, Submitted = 3, Accepted = 4, + Approved = 5, + Rejected = 6, Completed = 100 } } \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Data/Entities/EventLog.cs b/EnotaryoPH/EnotaryoPH.Data/Entities/EventLog.cs index f6740fd..2fc84d0 100644 --- a/EnotaryoPH/EnotaryoPH.Data/Entities/EventLog.cs +++ b/EnotaryoPH/EnotaryoPH.Data/Entities/EventLog.cs @@ -9,7 +9,7 @@ namespace EnotaryoPH.Data.Entities public int EventLogID { get; set; } [Column("StreamID")] - public int StreamID { get; set; } + public string StreamID { get; set; } [Column("LogType")] public string LogType { get; set; } diff --git a/EnotaryoPH/EnotaryoPH.Data/Entities/LawyerVideoConferenceSchedule.cs b/EnotaryoPH/EnotaryoPH.Data/Entities/LawyerVideoConferenceSchedule.cs index f59f893..d54ca04 100644 --- a/EnotaryoPH/EnotaryoPH.Data/Entities/LawyerVideoConferenceSchedule.cs +++ b/EnotaryoPH/EnotaryoPH.Data/Entities/LawyerVideoConferenceSchedule.cs @@ -43,6 +43,9 @@ namespace EnotaryoPH.Data.Entities [Column("Status")] public string? Status { get; set; } + [ForeignKey("TransactionID")] + public Transaction Transaction { get; set; } + [Column("TransactionID")] public int TransactionID { get; set; } diff --git a/EnotaryoPH/EnotaryoPH.Data/Entities/Transaction.cs b/EnotaryoPH/EnotaryoPH.Data/Entities/Transaction.cs index 7aee57c..9201014 100644 --- a/EnotaryoPH/EnotaryoPH.Data/Entities/Transaction.cs +++ b/EnotaryoPH/EnotaryoPH.Data/Entities/Transaction.cs @@ -29,6 +29,8 @@ namespace EnotaryoPH.Data.Entities [Column("PrincipalID")] public int PrincipalID { get; set; } + public LawyerVideoConferenceSchedule Schedule { get; set; } + [Column("Status")] public string Status { get; set; } diff --git a/EnotaryoPH/EnotaryoPH.Data/Entities/User.cs b/EnotaryoPH/EnotaryoPH.Data/Entities/User.cs index 1e48bf4..00c0b5d 100644 --- a/EnotaryoPH/EnotaryoPH.Data/Entities/User.cs +++ b/EnotaryoPH/EnotaryoPH.Data/Entities/User.cs @@ -5,49 +5,52 @@ namespace EnotaryoPH.Data.Entities [Table("Users")] public class User { - [Column("UserID")] - public int UserID { get; set; } + [Column("BirthDate")] + public DateTime BirthDate { get; set; } + + [Column("CreatedOn")] + public DateTime? CreatedOn { get; set; } [Column("Email")] public string Email { get; set; } + public List EventLogs { get; set; } + + [Column("Firstname")] + public string? Firstname { get; set; } + + [NotMapped] + public string Fullname => $"{Firstname} {Lastname}".Trim(); + + public List IdentificationDocuments { get; set; } + + [Column("Lastname")] + public string? Lastname { get; set; } + + public List LawyerVideoConferenceParticipants { get; set; } + + [Column("Middlename")] + public string? Middlename { get; set; } + [Column("PasswordHash")] public string PasswordHash { get; set; } [Column("PhoneNumber")] public string? PhoneNumber { get; set; } - [Column("Firstname")] - public string? Firstname { get; set; } - - [Column("Lastname")] - public string? Lastname { get; set; } - - [Column("CreatedOn")] - public DateTime? CreatedOn { get; set; } - - [Column("User_UID")] - public Guid? User_UID { get; set; } + [Column("Prefix")] + public string? Prefix { get; set; } [Column("Role")] public string? Role { get; set; } - [Column("BirthDate")] - public DateTime BirthDate { get; set; } - - [Column("Middlename")] - public string? Middlename { get; set; } - [Column("Suffix")] public string? Suffix { get; set; } - [Column("Prefix")] - public string? Prefix { get; set; } + [Column("User_UID")] + public Guid? User_UID { get; set; } - public List EventLogs { get; set; } - - public List IdentificationDocuments { get; set; } - - public List LawyerVideoConferenceParticipants { get; set; } + [Column("UserID")] + public int UserID { get; set; } } } \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Assets/js/Communication/VideoCall/_VideoCall.js b/EnotaryoPH/EnotaryoPH.Web/Assets/js/Communication/VideoCall/_VideoCall.js index 087f6b1..78548a6 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Assets/js/Communication/VideoCall/_VideoCall.js +++ b/EnotaryoPH/EnotaryoPH.Web/Assets/js/Communication/VideoCall/_VideoCall.js @@ -1,222 +1,301 @@ -import { AzureCommunicationTokenCredential } from '@azure/communication-common'; +import { AzureCommunicationTokenCredential, createIdentifierFromRawId } from '@azure/communication-common'; import { CallClient, LocalVideoStream, VideoStreamRenderer } from '@azure/communication-calling'; class VideoCall { constructor() { - this.callClient = null; - this.callAgent = null; - this.deviceManager = null; - this.localVideoStream = null; - this.call = null; - this.videoElement = document.createElement('video'); - this.videoElement.setAttribute('autoplay', ''); - this.videoElement.setAttribute('muted', ''); + //this.isLocalVideoStartedChangedCallback = null; + //this.localVideoStreamsUpdatedCallback = null; + //this.onCreateLocalVideoStreamCallback = null; + this.onGetServerCallIDCallback = null; + //this.remoteParticipantStateChangedCallback = null; + //this.remoteParticipantsUpdatedCallback = null; + //this.remoteVideoIsAvailableChangedCallback = null; this.stateChangedCallback = null; - this.remoteParticipantsUpdated = null; - this.isLocalVideoStartedChanged = null; - this.localVideoStreamsUpdated = null; - this.idChanged = null; - this.onCreateLocalVideoStream = null; - this.remoteParticipantStateChanged = null; - this.remoteVideoIsAvailableChanged = null; - this.onGetServerCallID = null; + this.callEndedCallback = null; + this.participantsJoinedCallback = null; + this.callsUpdatedCallback = null; + + this.callAdapter = null; + this.videoContainer = null; + this.displayName = null; + this.roomID = null; + this.token = null; + this.userID = null; + + this.onFetchParticipantMenuItemsCallback = null; + this.onFetchCustomButtonPropsCallbacks = []; + this.onAddParticipantCallback = null; } - async init(userAccessToken, options) { + async init(options) { + let self = this; this.stateChangedCallback = options.stateChangedCallback; - this.remoteParticipantsUpdated = options.remoteParticipantsUpdated; - this.isLocalVideoStartedChanged = options.isLocalVideoStartedChanged; - this.localVideoStreamsUpdated = options.localVideoStreamsUpdated; - this.idChanged = options.idChanged; - this.onCreateLocalVideoStream = options.onCreateLocalVideoStream; - this.remoteParticipantStateChanged = options.remoteParticipantStateChanged; - this.remoteVideoIsAvailableChanged = options.remoteVideoIsAvailableChanged; - this.onGetServerCallID = options.onGetServerCallID; + /*this.remoteParticipantsUpdatedCallback = options.remoteParticipantsUpdated;*/ + /*this.isLocalVideoStartedChangedCallback = options.isLocalVideoStartedChanged;*/ + /*this.localVideoStreamsUpdatedCallback = options.localVideoStreamsUpdated;*/ + this.idChangedCallback = options.idChanged; + //this.onCreateLocalVideoStreamCallback = options.onCreateLocalVideoStream; + //this.remoteParticipantStateChangedCallback = options.remoteParticipantStateChanged; + //this.remoteVideoIsAvailableChangedCallback = options.remoteVideoIsAvailableChanged; + this.onGetServerCallIDCallback = options.onGetServerCallIDCallback; + this.callEndedCallback = options.callEndedCallback; + this.participantsJoinedCallback = options.participantsJoinedCallback; + this.onFetchParticipantMenuItemsCallback = options.onFetchParticipantMenuItemsCallback; + this.onFetchCustomButtonPropsCallbacks = options.onFetchCustomButtonPropsCallbacks || []; + this.onAddParticipantCallback = options.onAddParticipantCallback; - const tokenCredential = new AzureCommunicationTokenCredential(userAccessToken); + this.callAdapter = options.callAdapter; + this.videoContainer = options.videoContainer; + this.displayName = options.displayName; + this.roomID = options.roomID; + this.token = options.token; + this.userID = options.userID; - this.callClient = new CallClient(); - this.callAgent = await this.callClient.createCallAgent(tokenCredential); + this.serverCallId; - this.deviceManager = await this.callClient.getDeviceManager(); - await this.deviceManager.askDevicePermission({ audio: true, video: true }); - const cameras = await this.deviceManager.getCameras(); - this.localVideoStream = new LocalVideoStream(cameras[0]); - } + const callControls = { + // Hide all default buttons + cameraButton: true, + endCallButton: false, + microphoneButton: false, + participantsButton: true, + screenShareButton: false, + devicesButton: false, + moreButton: false, + raiseHandButton: false, + reactionButton: false, + dtmfDialerButton: false, + holdButton: false, + peopleButton: false, + exitSpotlightButton: false, + captionsButton: false, + galleryControlsButton: false, + teamsMeetingPhoneCallButton: false, + displayType: 'compact', - stopLocalVideo() { - if (this.call) { - this.call.stopVideo(this.localVideoStream); - } - } + // Hide the entire control bar if needed + onFetchCustomButtonProps: this.onFetchCustomButtonPropsCallbacks + }; - async joinRoom(roomId) { - // Assuming you have a room ID and call the appropriate ACS API to join a room - // This is just a placeholder, replace with actual logic - console.log('Joining room:', roomId); - if (this.callAgent) { - try { - const localVideoStream = await this.createLocalVideoStream(); - const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined; - - this.callAgent.on('callsUpdated', e => { - e.added.forEach((addedCall) => { - addedCall.on('stateChanged', () => { - if (addedCall.state === 'Connected') { - addedCall.info.getServerCallId().then(result => { - this.onGetServerCallID?.(result); - }).catch(err => { - console.log(err); - }); - } - }); - }); - }); - - const roomCallLocator = { roomId: roomId }; - let call = this.callAgent.join(roomCallLocator, { videoOptions }); - this.subscribeToCall(call); - } catch (error) { - console.error(error); + const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); + const callCompositeProps = { + callControls: callControls, + formFactor: isMobile ? 'mobile' : 'desktop', + onFetchParticipantMenuItems: this.onFetchParticipantMenuItemsCallback, + options: { + callControls: callControls } - } - } + }; - async subscribeToCall(call) { - try { - call.on('idChanged', () => { - this.idChanged?.(call.id); - }); - call.on('stateChanged', async () => { - this.stateChangedCallback?.(call.state); - }); - call.on('isLocalVideoStartedChanged', () => { - this.isLocalVideoStartedChanged?.(call.isLocalVideoStarted); - }); - call.on('localVideoStreamsUpdated', e => { - this.localVideoStreamsUpdated?.(e); - }); + const adapterArgs = { + userId: createIdentifierFromRawId(this.userID), + //credential: new AzureCommunicationTokenCredential(this.token), + token: this.token, + displayName: this.displayName, + locator: { roomId: this.roomID }, + callAdapterOptions: {}, + callCompositeOptions: callCompositeProps + }; - // Subscribe to the call's 'remoteParticipantsUpdated' event to be - // notified when new participants are added to the call or removed from the call. - call.on('remoteParticipantsUpdated', e => { - this.remoteParticipantsUpdated?.(e); + this.callAdapter = await callComposite.loadCallComposite( + adapterArgs, + this.videoContainer, // container element, + callCompositeProps + ); - e.added.forEach(remoteParticipant => { - this.subscribeToRemoteParticipant(remoteParticipant) + //this.callAdapter.callAgent.on("callsUpdated", function (e) { + // e.added.forEach((addedCall) => { + // addedCall.on('stateChanged', (state) => this.stateChanged(addedCall)); + // }); + //}); + + this.callAdapter.on("callIdChanged", function (e) { + }); + + this.callAdapter.onStateChange(state => { + if (state.call?.info && !this.serverCallId) { + state.call.info.getServerCallId().then(result => { + this.serverCallId = result; + this.onGetServerCallIDCallback?.(result); + }).catch(err => { + console.log(err); }); - // Unsubscribe from participants that are removed from the call - e.removed.forEach(remoteParticipant => { - console.log('Remote participant removed from the call.'); - }); - }); - - call.localVideoStreams.forEach(async (lvs) => { - this.localVideoStream = lvs; - await this.displayLocalVideoStream(lvs); - }); - - // Inspect the call's current remote participants and subscribe to them. - call.remoteParticipants.forEach(remoteParticipant => { - this.subscribeToRemoteParticipant(remoteParticipant); - }); - } catch (error) { - console.error(error); - } - } - - subscribeToRemoteParticipant(remoteParticipant) { - try { - // Inspect the initial remoteParticipant.state value. - console.log(`Remote participant state: ${remoteParticipant.state}`); - // Subscribe to remoteParticipant's 'stateChanged' event for value changes. - remoteParticipant.on('stateChanged', () => { - console.log(`Remote participant state changed: ${remoteParticipant.state}`, JSON.stringify(remoteParticipant)); - this.remoteParticipantStateChanged?.(remoteParticipant.state); - }); - - // Inspect the remoteParticipants's current videoStreams and subscribe to them. - remoteParticipant.videoStreams.forEach(remoteVideoStream => { - this.subscribeToRemoteVideoStream(remoteVideoStream); - }); - // Subscribe to the remoteParticipant's 'videoStreamsUpdated' event to be - // notified when the remoteParticipant adds new videoStreams and removes video streams. - remoteParticipant.on('videoStreamsUpdated', e => { - // Subscribe to new remote participant's video streams that were added. - e.added.forEach(remoteVideoStream => { - this.subscribeToRemoteVideoStream(remoteVideoStream); - }); - // Unsubscribe from remote participant's video streams that were removed. - e.removed.forEach(remoteVideoStream => { - console.log('Remote participant video stream was removed.'); - }) - }); - } catch (error) { - console.error(error); - } - } - - async subscribeToRemoteVideoStream(remoteVideoStream) { - let renderer = new VideoStreamRenderer(remoteVideoStream); - let view; - - const createView = async () => { - // Create a renderer view for the remote video stream. - view = await renderer.createView(); - this.remoteVideoIsAvailableChanged?.({ - isAvailable: remoteVideoStream.isAvailable, - participantId: remoteVideoStream.tsParticipantId, - el: view.target - }); - } - - // Remote participant has switched video on/off - remoteVideoStream.on('isAvailableChanged', async () => { - try { - if (remoteVideoStream.isAvailable) { - await createView(); - } else { - view?.dispose(); - } - } catch (e) { - console.error(e); } }); - // Remote participant has video on initially. - if (remoteVideoStream.isAvailable) { - try { - await createView(); - } catch (e) { - console.error(e); - } - } + this.callAdapter.on("callEnded", function (e) { + self.callEndedCallback?.(e); + }); + + this.callAdapter.on("participantsJoined", function (e, f) { + self.participantsJoinedCallback?.(e, f); + }); + + this.callAdapter.on("onAddParticipant", function (e, f) { + self.onAddParticipantCallback?.(e, f); + }); + + //CallEnded + + return this.callAdapter; } - async createLocalVideoStream() { - const camera = (await this.deviceManager.getCameras())[0]; - if (camera) { - return new LocalVideoStream(camera); - } else { - console.error(`No camera device found on the system`); - } + //stopLocalVideo() { + // if (this.call) { + // this.call.stopVideo(this.localVideoStream); + // } + //} + + async joinRoom() { + await this.callAdapter?.joinCall({ + microphoneOn: true, + cameraOn: true + }); } + async stopCall(forEveryone = false) { + await this.callAdapter?.leaveCall(forEveryone); + } + + //async subscribeToCall(call) { + // try { + // call.on('idChanged', () => { + // this.idChanged?.(call.id); + // }); + // call.on('stateChanged', async () => { + // this.stateChangedCallback?.(call.state); + // }); + // call.on('isLocalVideoStartedChanged', () => { + // this.isLocalVideoStartedChanged?.(call.isLocalVideoStarted); + // }); + // call.on('localVideoStreamsUpdated', e => { + // this.localVideoStreamsUpdated?.(e); + // }); + + // // Subscribe to the call's 'remoteParticipantsUpdated' event to be + // // notified when new participants are added to the call or removed from the call. + // call.on('remoteParticipantsUpdated', e => { + // this.remoteParticipantsUpdated?.(e); + + // e.added.forEach(remoteParticipant => { + // this.subscribeToRemoteParticipant(remoteParticipant) + // }); + // // Unsubscribe from participants that are removed from the call + // e.removed.forEach(remoteParticipant => { + // console.log('Remote participant removed from the call.'); + // }); + // }); + + // call.localVideoStreams.forEach(async (lvs) => { + // this.localVideoStream = lvs; + // await this.displayLocalVideoStream(lvs); + // }); + + // // Inspect the call's current remote participants and subscribe to them. + // call.remoteParticipants.forEach(remoteParticipant => { + // this.subscribeToRemoteParticipant(remoteParticipant); + // }); + // } catch (error) { + // console.error(error); + // } + //} + + //subscribeToRemoteParticipant(remoteParticipant) { + // try { + // // Inspect the initial remoteParticipant.state value. + // console.log(`Remote participant state: ${remoteParticipant.state}`); + // // Subscribe to remoteParticipant's 'stateChanged' event for value changes. + // remoteParticipant.on('stateChanged', () => { + // console.log(`Remote participant state changed: ${remoteParticipant.state}`, JSON.stringify(remoteParticipant)); + // this.remoteParticipantStateChanged?.(remoteParticipant.state); + // }); + + // // Inspect the remoteParticipants's current videoStreams and subscribe to them. + // remoteParticipant.videoStreams.forEach(remoteVideoStream => { + // this.subscribeToRemoteVideoStream(remoteVideoStream); + // }); + // // Subscribe to the remoteParticipant's 'videoStreamsUpdated' event to be + // // notified when the remoteParticipant adds new videoStreams and removes video streams. + // remoteParticipant.on('videoStreamsUpdated', e => { + // // Subscribe to new remote participant's video streams that were added. + // e.added.forEach(remoteVideoStream => { + // this.subscribeToRemoteVideoStream(remoteVideoStream); + // }); + // // Unsubscribe from remote participant's video streams that were removed. + // e.removed.forEach(remoteVideoStream => { + // console.log('Remote participant video stream was removed.'); + // }) + // }); + // } catch (error) { + // console.error(error); + // } + //} + + //async subscribeToRemoteVideoStream(remoteVideoStream) { + // let renderer = new VideoStreamRenderer(remoteVideoStream); + // let view; + + // const createView = async () => { + // // Create a renderer view for the remote video stream. + // view = await renderer.createView(); + // this.remoteVideoIsAvailableChanged?.({ + // isAvailable: remoteVideoStream.isAvailable, + // participantId: remoteVideoStream.tsParticipantId, + // el: view.target + // }); + // } + + // // Remote participant has switched video on/off + // remoteVideoStream.on('isAvailableChanged', async () => { + // try { + // if (remoteVideoStream.isAvailable) { + // await createView(); + // } else { + // view?.dispose(); + // } + // } catch (e) { + // console.error(e); + // } + // }); + + // // Remote participant has video on initially. + // if (remoteVideoStream.isAvailable) { + // try { + // await createView(); + // } catch (e) { + // console.error(e); + // } + // } + //} + + //async createLocalVideoStream() { + // const camera = (await this.deviceManager.getCameras())[0]; + // if (camera) { + // return new LocalVideoStream(camera); + // } else { + // console.error(`No camera device found on the system`); + // } + //} + leaveRoom() { if (this.call) { this.call.hangUp(); } } - async displayLocalVideoStream(lvs) { - try { - let localVideoStreamRenderer = new VideoStreamRenderer(lvs); - const view = await localVideoStreamRenderer.createView(); - this.onCreateLocalVideoStream?.(view.target); - } catch (error) { - console.error(error); - } - } + //createIdentifierFromRawId(rawId) { + // return createIdentifierFromRawId(rawId); + //} + + //async displayLocalVideoStream(lvs) { + // try { + // let localVideoStreamRenderer = new VideoStreamRenderer(lvs); + // const view = await localVideoStreamRenderer.createView(); + // this.onCreateLocalVideoStream?.(view.target); + // } catch (error) { + // console.error(error); + // } + //} } export default VideoCall; \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Exceptions/NoDataException.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Exceptions/NoDataException.cs new file mode 100644 index 0000000..f1443f1 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Exceptions/NoDataException.cs @@ -0,0 +1,39 @@ +namespace EnotaryoPH.Web.Common.Exceptions +{ + public class NoDataException : Exception + { + public NoDataException(string typeName, object id) : base($"No data found for {typeName} with id = '{id}'.") + { + TypeName = typeName; + ID = id; + Key = string.Empty; + } + + public NoDataException(string typeName, object id, string key) : base($"No data found for {typeName} with {key} = '{id}'.") + { + TypeName = typeName; + ID = id; + Key = key; + } + + public static void ThrowIfNull(object data, string typeName, object id) + { + if (data == null) + { + throw new NoDataException(typeName, id); + } + } + + public static void ThrowIfNull(object data, string typeName, object id, string key) + { + if (data == null) + { + throw new NoDataException(typeName, id, key); + } + } + + public object ID { get; private set; } + public string Key { get; private set; } + public string TypeName { get; private set; } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Extensions/IEnumerableExtensions.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Extensions/IEnumerableExtensions.cs new file mode 100644 index 0000000..c7f1ce1 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,9 @@ +namespace EnotaryoPH.Web.Common.Extensions +{ + public static class IEnumerableExtensions + { + public static IEnumerable ToSafeEnumerable(this IEnumerable enumerable) => enumerable ?? Enumerable.Empty(); + + public static List ToSafeList(this IEnumerable enumerable) => enumerable?.ToList() ?? []; + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Extensions/ObjectExtensions.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..cf5c1eb --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Extensions/ObjectExtensions.cs @@ -0,0 +1,11 @@ +namespace EnotaryoPH.Web.Common.Extensions +{ + public static class ObjectExtensions + { + public static int ToInteger(this object obj) + { + int.TryParse(obj.ToString(), out var result); + return result; + } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Extensions/StringExtensions.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Extensions/StringExtensions.cs index 6523612..d5fe801 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Common/Extensions/StringExtensions.cs +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Extensions/StringExtensions.cs @@ -6,7 +6,7 @@ private const char Plus = '+'; private const char Slash = '/'; - public static string DefaultIfEmpty(this string s, string defaultValue) => !string.IsNullOrWhiteSpace(s) ? s : (defaultValue ?? string.Empty); + public static string DefaultIfEmpty(this string s, string defaultValue) => !string.IsNullOrWhiteSpace(s) ? s : defaultValue ?? string.Empty; public static bool IsInList(this string s, params string[] list) => list.Contains(s, StringComparer.OrdinalIgnoreCase); diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/CheckRecordingAvailabilityInvocable.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/CheckRecordingAvailabilityInvocable.cs new file mode 100644 index 0000000..13b7569 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/CheckRecordingAvailabilityInvocable.cs @@ -0,0 +1,85 @@ +using System.Text; +using System.Text.Json; +using Azure.Communication.CallAutomation; +using Azure.Storage.Queues; +using Coravel.Invocable; +using EnotaryoPH.Data; +using EnotaryoPH.Data.Entities; +using EnotaryoPH.Web.Common.Jobs.Models; + +namespace EnotaryoPH.Web.Common.Jobs +{ + public class CheckRecordingAvailabilityInvocable : IInvocable + { + private readonly QueueClient _queueClient; + private readonly CallAutomationClient _callAutomationClient; + private readonly IConfiguration _configuration; + private readonly IServiceScopeFactory _serviceScopeFactory; + + public CheckRecordingAvailabilityInvocable(QueueClient queueClient, CallAutomationClient callAutomationClient, IConfiguration configuration, IServiceScopeFactory serviceScopeFactory) + { + _queueClient = queueClient; + _callAutomationClient = callAutomationClient; + _configuration = configuration; + _serviceScopeFactory = serviceScopeFactory; + } + + public async Task Invoke() + { + var message = await _queueClient.ReceiveMessageAsync(); + if (message.Value == null) + { + return; + } + var base64EncodedData = message.Value.Body.ToString(); + var base64EncodedBytes = Convert.FromBase64String(base64EncodedData); + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + var json = Encoding.UTF8.GetString(base64EncodedBytes); + var model = JsonSerializer.Deserialize(json, options); + if (model?.Data?.RecordingStorageInfo?.RecordingChunks?.Count > 0) + { + var dbContext = _serviceScopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + model.Data.RecordingStorageInfo.RecordingChunks.ForEach(async chunk => + { + var path = _configuration.GetValue("VideoRecordingsLocation"); + if (!Path.Exists(path)) + { + Directory.CreateDirectory(path); + } + var fileName = Path.Combine(path, $"{chunk.DocumentId}-{chunk.Index}.mp4"); + if (File.Exists(fileName)) + { + File.Move(fileName, fileName.Replace(".mp4", $"_{DateTime.UtcNow.ToString("yyyy-MM-dd-HH-mm-dd")}.mp4")); + } + + using var memoryStream = new MemoryStream(); + await _callAutomationClient + .GetCallRecording().DownloadToAsync(new Uri(chunk.ContentLocation), fileName); + + var schedule = dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.RecordingID == model.Data.RecordingId); + if (schedule != null) + { + if (schedule.VideoRecording == null) + { + schedule.VideoRecording = new VideoRecording + { + CreatedOn = DateTime.UtcNow, + LocationType = nameof(VideoRecordingLocationType.LocalFolder), + VideoConferenceScheduleID = schedule.LawyerVideoConferenceScheduleID, + VideoRecording_UID = Guid.CreateVersion7(DateTime.UtcNow) + }; + } + schedule.VideoRecording.Path = fileName; + schedule.VideoRecording.Metadata = JsonSerializer.Serialize(json); + dbContext.UpdateOrCreate(schedule); + dbContext.SaveChanges(); + } + }); + } + await _queueClient.DeleteMessageAsync(message.Value.MessageId, message.Value.PopReceipt); + } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/Data.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/Data.cs new file mode 100644 index 0000000..07b4412 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/Data.cs @@ -0,0 +1,12 @@ +namespace EnotaryoPH.Web.Common.Jobs.Models +{ + public class Data + { + public int RecordingDurationMs { get; set; } + public string RecordingId { get; set; } + public DateTime RecordingStartTime { get; set; } + public RecordingStorageInfo RecordingStorageInfo { get; set; } + public string SessionEndReason { get; set; } + public string StorageType { get; set; } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/RecordingChunk.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/RecordingChunk.cs new file mode 100644 index 0000000..2ac364c --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/RecordingChunk.cs @@ -0,0 +1,12 @@ +namespace EnotaryoPH.Web.Common.Jobs.Models +{ + public class RecordingChunk + { + public string ContentLocation { get; set; } + public string DeleteLocation { get; set; } + public string DocumentId { get; set; } + public string EndReason { get; set; } + public int Index { get; set; } + public string MetadataLocation { get; set; } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/RecordingFileStatusUpdatedModel.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/RecordingFileStatusUpdatedModel.cs new file mode 100644 index 0000000..1a76076 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/RecordingFileStatusUpdatedModel.cs @@ -0,0 +1,14 @@ +namespace EnotaryoPH.Web.Common.Jobs.Models +{ + public class RecordingFileStatusUpdatedModel + { + public Data Data { get; set; } + public string DataVersion { get; set; } + public DateTime EventTime { get; set; } + public string EventType { get; set; } + public string Id { get; set; } + public string MetadataVersion { get; set; } + public string Subject { get; set; } + public string Topic { get; set; } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/RecordingStorageInfo.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/RecordingStorageInfo.cs new file mode 100644 index 0000000..ac81c6c --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/RecordingStorageInfo.cs @@ -0,0 +1,7 @@ +namespace EnotaryoPH.Web.Common.Jobs.Models +{ + public class RecordingStorageInfo + { + public List RecordingChunks { get; set; } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Services/ConferenceSheduleService.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Services/ConferenceSheduleService.cs deleted file mode 100644 index ba412f1..0000000 --- a/EnotaryoPH/EnotaryoPH.Web/Common/Services/ConferenceSheduleService.cs +++ /dev/null @@ -1,175 +0,0 @@ -using Azure; -using Azure.Communication; -using Azure.Communication.CallAutomation; -using Azure.Communication.Identity; -using Azure.Communication.Rooms; -using EnotaryoPH.Data; -using EnotaryoPH.Data.Entities; - -namespace EnotaryoPH.Web.Common.Services -{ - public class ConferenceSheduleService : IConferenceSheduleService - { - private readonly NotaryoDBContext _dbContext; - private readonly CommunicationIdentityClient _communicationIdentityClient; - private readonly RoomsClient _roomsClient; - private readonly CallAutomationClient _callAutomationClient; - private readonly IConfiguration _configuration; - - public ConferenceSheduleService(NotaryoDBContext dbContext, CommunicationIdentityClient communicationIdentityClient, RoomsClient roomsClient, CallAutomationClient callAutomationClient, IConfiguration configuration) - { - _dbContext = dbContext; - _communicationIdentityClient = communicationIdentityClient; - _roomsClient = roomsClient; - _callAutomationClient = callAutomationClient; - _configuration = configuration; - } - - public async Task GetOrCreateScheduleIDAsync(Guid transaction_UID) - { - if (transaction_UID == Guid.Empty) - { - return Guid.Empty; - } - - var transactionEntity = _dbContext.Transactions - .Include(t => t.TransactionSignatories) - .FirstOrDefault(t => t.Transaction_UID == transaction_UID); - if (transactionEntity == null) - { - return Guid.Empty; - } - - var existingSchedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); - if (existingSchedule != null) - { - return existingSchedule.LawyerVideoConferenceSchedule_UID; - } - - var schedule_UID = Guid.Empty; - var isReadyForVideoCall = transactionEntity.TransactionSignatories.All(signatory => signatory.Status == nameof(SignatoryStatus.FaceMatch)); - var isAcceptedByLawyer = transactionEntity.Status == nameof(TransactionState.Accepted) && transactionEntity.LawyerID > 0; - if (isReadyForVideoCall && isAcceptedByLawyer) - { - var schedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); - if (schedule == null) - { - var participants = transactionEntity.TransactionSignatories.ConvertAll(signatory => new LawyerVideoConferenceParticipant - { - CreatedOn = DateTime.UtcNow, - Status = nameof(VideoConferenceStatus.New), - LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow), - ParticipantID = signatory.UserID, - }); - - participants.Add(new LawyerVideoConferenceParticipant - { - CreatedOn = DateTime.UtcNow, - Status = nameof(VideoConferenceStatus.New), - LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow), - ParticipantID = transactionEntity.PrincipalID, - }); - - schedule = new LawyerVideoConferenceSchedule - { - LawyerVideoConferenceSchedule_UID = Guid.CreateVersion7(DateTime.UtcNow), - CreatedOn = DateTime.UtcNow, - LawyerID = transactionEntity.LawyerID.GetValueOrDefault(), - TransactionID = transactionEntity.TransactionID, - MeetingDate = DateTime.UtcNow, - Status = nameof(VideoConferenceStatus.New), - }; - - var roomParticipants = new List(); - foreach (var participant in participants) - { - var attendee = await _communicationIdentityClient.CreateUserAsync(); - participant.MeetingRoomTokenID = await GetTokenResponseAsync(attendee); - participant.MeetingRoomUserID = attendee.Value.Id; - roomParticipants.Add(new RoomParticipant(attendee) { Role = ParticipantRole.Attendee }); - } - - var presenter = await _communicationIdentityClient.CreateUserAsync(); - schedule.MeetingRoomTokenID = await GetTokenResponseAsync(presenter); - schedule.MeetingRoomUserID = presenter.Value.Id; - roomParticipants.Add(new RoomParticipant(presenter) { Role = ParticipantRole.Presenter }); - - CommunicationRoom room = await _roomsClient.CreateRoomAsync(DateTime.Now, DateTime.Now.AddHours(2), roomParticipants); - schedule.MeetingRoomID = room.Id; - schedule.LawyerVideoConferenceParticipants = participants.ToList(); - - if (schedule.LawyerVideoConferenceScheduleID == 0) - { - _dbContext.Add(schedule); - } - else - { - _dbContext.Update(schedule); - } - _dbContext.SaveChanges(); - schedule_UID = schedule.LawyerVideoConferenceSchedule_UID; - } - } - - return schedule_UID; - } - - public async Task StartRecordingAsync(Guid transaction_UID, string serverCallID) - { - var transactionEntity = _dbContext.Transactions - .FirstOrDefault(t => t.Transaction_UID == transaction_UID) ?? throw new ArgumentException("Transaction not found."); - - var existingSchedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID) ?? throw new ArgumentException("Schedule not found."); - if (string.IsNullOrEmpty(existingSchedule.ServerCallID) && string.IsNullOrEmpty(serverCallID)) - { - throw new ArgumentException("ServerCallID is not set for this transaction."); - } - - if (!string.IsNullOrEmpty(existingSchedule.RecordingID)) - { - return; - } - - if (!string.IsNullOrEmpty(serverCallID)) - { - existingSchedule.ServerCallID = serverCallID; - } - - CallLocator callLocator = new ServerCallLocator(existingSchedule.ServerCallID); - var uri = _configuration.GetValue("UriRecordingBloblContainer") ?? string.Empty; - var recordingResult = await _callAutomationClient - .GetCallRecording().StartAsync(new StartRecordingOptions(callLocator) - { - RecordingStorage = RecordingStorage.CreateAzureBlobContainerRecordingStorage(new Uri(uri)) - }); - existingSchedule.RecordingID = recordingResult.Value.RecordingId; - _dbContext.Update(existingSchedule); - _dbContext.SaveChanges(); - } - - public async Task StopRecordingAsync(Guid transaction_UID) - { - var transactionEntity = _dbContext.Transactions - .FirstOrDefault(t => t.Transaction_UID == transaction_UID) ?? throw new ArgumentException("Transaction not found."); - - var existingSchedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID) ?? throw new ArgumentException("Schedule not found."); - if (string.IsNullOrEmpty(existingSchedule.ServerCallID)) - { - throw new ArgumentException("ServerCallID is not set for this transaction."); - } - - if (string.IsNullOrEmpty(existingSchedule.RecordingID)) - { - throw new ArgumentException("Recording ID is not set for this transaction."); - } - - await _callAutomationClient.GetCallRecording().StopAsync(existingSchedule.RecordingID); - } - - private async Task GetTokenResponseAsync(Response user) - { - var tokenResponse = await _communicationIdentityClient.GetTokenAsync(user, new[] { CommunicationTokenScope.VoIP }); - return tokenResponse.Value.Token; - } - } -} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Services/EventService.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Services/EventService.cs new file mode 100644 index 0000000..989619e --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Services/EventService.cs @@ -0,0 +1,63 @@ +using System.Text.Json; +using EnotaryoPH.Data; +using EnotaryoPH.Data.Entities; + +namespace EnotaryoPH.Web.Common.Services +{ + public class EventService : IEventService + { + private readonly NotaryoDBContext _dBContext; + private readonly INotificationService _notificationService; + + public EventService(NotaryoDBContext dBContext, INotificationService notificationService) + { + _dBContext = dBContext; + _notificationService = notificationService; + } + + public Task LogAsync(NotaryoEvent notaryoEvent, object entityId) => LogAsync(notaryoEvent, entityId, null); + + public async Task LogAsync(NotaryoEvent notaryoEvent, List entityIds, object payLoad) + { + foreach (var entityId in entityIds) + { + await LogAsync(notaryoEvent, entityId, payLoad); + } + } + + public async Task LogAsync(NotaryoEvent notaryoEvent, object entityId, object payLoad) + { + var logItem = new EventLog + { + Description = $"Event: {notaryoEvent}, Entity: {entityId}", + EventLog_UID = Guid.CreateVersion7(DateTime.UtcNow), + LogDate = DateTime.UtcNow, + LogType = notaryoEvent.ToString(), + Payload = payLoad != null ? JsonSerializer.Serialize(payLoad) : null, + StreamID = entityId.ToString(), + }; + if (notaryoEvent == NotaryoEvent.TransactionApproved) + { + var transaction = _dBContext.Transactions.AsNoTracking() + .Include(t => t.TransactionSignatories) + .ThenInclude(ts => ts.User) + .Include(t => t.Lawyer) + .ThenInclude(l => l.User) + .Include(t => t.Principal) + .Include(t => t.TransactionDocument) + .FirstOrDefault(t => t.TransactionID == entityId.ToInteger()); + logItem.Description = $"Transaction {entityId} has been approved."; + if (transaction != null) + { + var message = $"The document {transaction.TransactionDocument.DocumentType} has been approved."; + var allUsers = transaction.TransactionSignatories.ConvertAll(ts => ts.User.Email); + allUsers.Add(transaction.Lawyer.User.Email); + allUsers.Add(transaction.Principal.Email); + await _notificationService.NotifyUsersAsync(message, allUsers.ToArray()); + } + } + _dBContext.Add(logItem); + _dBContext.SaveChanges(); + } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Services/IConferenceSheduleService.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Services/IConferenceSheduleService.cs deleted file mode 100644 index ec03b12..0000000 --- a/EnotaryoPH/EnotaryoPH.Web/Common/Services/IConferenceSheduleService.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace EnotaryoPH.Web.Common.Services -{ - public interface IConferenceSheduleService - { - Task GetOrCreateScheduleIDAsync(Guid transaction_UID); - - Task StartRecordingAsync(Guid transaction_UID, string serverCallID); - - Task StopRecordingAsync(Guid transaction_UID); - } -} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Services/IEventService.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Services/IEventService.cs new file mode 100644 index 0000000..97ccd7c --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Services/IEventService.cs @@ -0,0 +1,11 @@ +namespace EnotaryoPH.Web.Common.Services +{ + public interface IEventService + { + Task LogAsync(NotaryoEvent notaryoEvent, List entityIds, object payLoad); + + Task LogAsync(NotaryoEvent notaryoEvent, object entityId); + + Task LogAsync(NotaryoEvent notaryoEvent, object entityId, object payLoad); + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Services/INotificationService.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Services/INotificationService.cs new file mode 100644 index 0000000..37b7249 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Services/INotificationService.cs @@ -0,0 +1,10 @@ + +namespace EnotaryoPH.Web.Common.Services +{ + public interface INotificationService + { + Task NotifyAllAsync(string message); + Task NotifyUserAsync(string message, string userID); + Task NotifyUsersAsync(string message, params string[] userIDs); + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Services/IVideoConferenceService.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Services/IVideoConferenceService.cs new file mode 100644 index 0000000..20b07b4 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Services/IVideoConferenceService.cs @@ -0,0 +1,17 @@ +namespace EnotaryoPH.Web.Common.Services +{ + public interface IVideoConferenceService + { + Task ApproveTransactionAsync(Guid transaction_UID); + + bool CanStart(Guid transaction_UID); + + Guid GetUIDByTransactionUID(Guid transaction_UID); + + bool HasExpired(Guid transaction_UID); + + Task StartAsync(Guid transaction_UID); + + Task StartRecordingAsync(Guid transaction_UID, string serverCallID); + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Services/NotificationService.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Services/NotificationService.cs index 854e664..8379cba 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Common/Services/NotificationService.cs +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Services/NotificationService.cs @@ -3,12 +3,16 @@ using Microsoft.AspNetCore.SignalR; namespace EnotaryoPH.Web.Common.Services { - public class NotificationService + public class NotificationService : INotificationService { private readonly IHubContext _hubContext; public NotificationService(IHubContext hubContext) => _hubContext = hubContext; - public async Task NotifyUserAsync(string user_UID, string message) => await _hubContext.Clients.All.SendAsync("ReceiveUserNotification", user_UID, message); + public async Task NotifyAllAsync(string message) => await _hubContext.Clients.All.SendAsync("ReceiveUserNotification", message); + + public async Task NotifyUserAsync(string message, string userID) => await NotifyUsersAsync(message, userID); + + public async Task NotifyUsersAsync(string message, params string[] userIDs) => await _hubContext.Clients.Users(userIDs).SendAsync("ReceiveUserNotification", message); } } \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Services/VideoConferenceService.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Services/VideoConferenceService.cs new file mode 100644 index 0000000..94e7eed --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Services/VideoConferenceService.cs @@ -0,0 +1,256 @@ +using Azure; +using Azure.Communication; +using Azure.Communication.CallAutomation; +using Azure.Communication.Identity; +using Azure.Communication.Rooms; +using EnotaryoPH.Data; +using EnotaryoPH.Data.Entities; + +namespace EnotaryoPH.Web.Common.Services +{ + public class VideoConferenceService : IVideoConferenceService + { + private const int VideoConferenceExpirationInHours = 2; + + private readonly CallAutomationClient _callAutomationClient; + private readonly CommunicationIdentityClient _communicationIdentityClient; + private readonly IConfiguration _configuration; + private readonly NotaryoDBContext _dbContext; + private readonly IEventService _eventService; + private readonly RoomsClient _roomsClient; + + public VideoConferenceService(NotaryoDBContext dbContext, CommunicationIdentityClient communicationIdentityClient, RoomsClient roomsClient, CallAutomationClient callAutomationClient, IConfiguration configuration, IEventService eventService) + { + _dbContext = dbContext; + _communicationIdentityClient = communicationIdentityClient; + _roomsClient = roomsClient; + _callAutomationClient = callAutomationClient; + _configuration = configuration; + _eventService = eventService; + } + + public async Task ApproveTransactionAsync(Guid transaction_UID) + { + var transaction = _dbContext.Transactions + .Include(t => t.TransactionSignatories) + .ThenInclude(ts => ts.User) + .Include(t => t.Schedule) + .Include(t => t.Lawyer) + .ThenInclude(l => l.User) + .Include(t => t.Principal) + .FirstOrDefault(t => t.Transaction_UID == transaction_UID); + NoDataException.ThrowIfNull(transaction, nameof(Transaction), transaction_UID); + transaction.Status = nameof(TransactionState.Approved); + transaction.TransactionSignatories.ForEach(ts => ts.Status = nameof(SignatoryStatus.Approved)); + transaction.Schedule.Status = nameof(VideoConferenceStatus.Completed); + _dbContext.Update(transaction); + _dbContext.SaveChanges(); + + await Task.WhenAll( + _eventService.LogAsync(NotaryoEvent.TransactionApproved, transaction_UID), + StopRecordingAsync(transaction.Schedule) + ); + } + + public bool CanStart(Guid transaction_UID) + { + if (transaction_UID == Guid.Empty) + { + return false; + } + + var transactionEntity = _dbContext.Transactions + .AsNoTracking() + .Include(t => t.TransactionSignatories) + .FirstOrDefault(t => t.Transaction_UID == transaction_UID); + if (transactionEntity == null) + { + return false; + } + + var isReadyForVideoCall = transactionEntity.TransactionSignatories.All(signatory => signatory.Status == nameof(SignatoryStatus.FaceMatch)); + var isAcceptedByLawyer = transactionEntity.Status == nameof(TransactionState.Accepted) && transactionEntity.LawyerID > 0; + return isReadyForVideoCall && isAcceptedByLawyer; + } + + public Guid GetUIDByTransactionUID(Guid transaction_UID) + { + var transactionEntity = _dbContext.Transactions + .AsNoTracking() + .FirstOrDefault(t => t.Transaction_UID == transaction_UID); + if (transactionEntity == null) + { + return Guid.Empty; + } + + var schedule = _dbContext.LawyerVideoConferenceSchedules.AsNoTracking().FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); + return schedule == null ? Guid.Empty : schedule.LawyerVideoConferenceSchedule_UID; + } + + public bool HasExpired(Guid transaction_UID) + { + var transactionEntity = _dbContext.Transactions + .Include(t => t.TransactionSignatories) + .FirstOrDefault(t => t.Transaction_UID == transaction_UID); + if (transactionEntity == null) + { + return false; + } + var schedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); + if (schedule == null) + { + return false; + } + if (schedule.Status == nameof(VideoConferenceStatus.Expired)) + { + return true; + } + if ((DateTime.UtcNow - schedule.MeetingDate).TotalHours > VideoConferenceExpirationInHours) + { + if (!schedule.Status?.IsInList(VideoConferenceStatus.Abandoned, VideoConferenceStatus.Completed) ?? false) + { + schedule.Status = nameof(VideoConferenceStatus.Expired); + _dbContext.Update(schedule); + _dbContext.SaveChanges(); + } + return true; + } + return false; + } + + public async Task StartAsync(Guid transaction_UID) + { + if (!CanStart(transaction_UID)) + { + throw new ArgumentException("Transaction is not ready for video conference."); + } + var schedule = GetOrCreateNewSchedule(transaction_UID); + if (schedule.Status == nameof(VideoConferenceStatus.New)) + { + await CreateMeetingRoomAsync(schedule); + _dbContext.UpdateOrCreate(schedule); + _dbContext.SaveChanges(); + } + return schedule.LawyerVideoConferenceSchedule_UID; + } + + public async Task StartRecordingAsync(Guid transaction_UID, string serverCallID) + { + var transactionEntity = _dbContext.Transactions + .Include(t => t.TransactionSignatories) + .Single(t => t.Transaction_UID == transaction_UID); + NoDataException.ThrowIfNull(transactionEntity, nameof(Transaction), transaction_UID); + + var schedule = _dbContext.LawyerVideoConferenceSchedules + .FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); + NoDataException.ThrowIfNull(schedule, nameof(LawyerVideoConferenceSchedule), transactionEntity.TransactionID, FullName.Of(transactionEntity.TransactionID)); + + if (string.IsNullOrEmpty(schedule.RecordingID)) + { + schedule.RecordingID = await StartRecordingAsync(serverCallID); + schedule.ServerCallID = serverCallID; + _dbContext.Update(schedule); + _dbContext.SaveChanges(); + } + } + + private async Task CreateMeetingRoomAsync(LawyerVideoConferenceSchedule schedule) + { + var roomParticipants = new List(); + foreach (var participant in schedule.LawyerVideoConferenceParticipants.ToSafeList()) + { + var attendee = await _communicationIdentityClient.CreateUserAsync(); + participant.MeetingRoomTokenID = await GetTokenResponseAsync(attendee); + participant.MeetingRoomUserID = attendee.Value.Id; + roomParticipants.Add(new RoomParticipant(attendee) { Role = ParticipantRole.Attendee }); + } + + var presenter = await _communicationIdentityClient.CreateUserAsync(); + schedule.MeetingRoomTokenID = await GetTokenResponseAsync(presenter); + schedule.MeetingRoomUserID = presenter.Value.Id; + roomParticipants.Add(new RoomParticipant(presenter) { Role = ParticipantRole.Presenter }); + + CommunicationRoom room = await _roomsClient.CreateRoomAsync(DateTime.Now, DateTime.Now.AddHours(2), roomParticipants); + schedule.MeetingRoomID = room.Id; + schedule.Status = nameof(VideoConferenceStatus.InProgress); + } + + private LawyerVideoConferenceSchedule GetOrCreateNewSchedule(Guid transaction_UID) + { + var transactionEntity = _dbContext.Transactions + .Include(t => t.TransactionSignatories) + .Single(t => t.Transaction_UID == transaction_UID); + + var schedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); + if (schedule != null) + { + return schedule; + } + schedule = new LawyerVideoConferenceSchedule + { + LawyerVideoConferenceSchedule_UID = Guid.CreateVersion7(DateTime.UtcNow), + CreatedOn = DateTime.UtcNow, + LawyerID = transactionEntity.LawyerID.GetValueOrDefault(), + TransactionID = transactionEntity.TransactionID, + Status = nameof(VideoConferenceStatus.New) + }; + + var participants = transactionEntity.TransactionSignatories.ConvertAll(signatory => new LawyerVideoConferenceParticipant + { + CreatedOn = DateTime.UtcNow, + Status = nameof(VideoConferenceStatus.New), + LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow), + ParticipantID = signatory.UserID, + }); + participants.Add(new LawyerVideoConferenceParticipant + { + CreatedOn = DateTime.UtcNow, + Status = nameof(VideoConferenceStatus.New), + LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow), + ParticipantID = transactionEntity.PrincipalID, + }); + + schedule.MeetingDate = DateTime.UtcNow; + schedule.LawyerVideoConferenceParticipants = participants.ToList(); + return schedule; + } + + private async Task GetTokenResponseAsync(Response user) + { + var tokenResponse = await _communicationIdentityClient.GetTokenAsync(user, new[] { CommunicationTokenScope.VoIP }); + return tokenResponse.Value.Token; + } + + private async Task StartRecordingAsync(string serverCallID) + { + ArgumentException.ThrowIfNullOrWhiteSpace(serverCallID); + CallLocator callLocator = new ServerCallLocator(serverCallID); + var uri = _configuration.GetValue("UriRecordingBlobContainer") ?? string.Empty; + var recordingResult = await _callAutomationClient + .GetCallRecording().StartAsync(new StartRecordingOptions(callLocator) + { + RecordingContent = RecordingContent.AudioVideo, + RecordingStorage = RecordingStorage.CreateAzureBlobContainerRecordingStorage(new Uri(uri)), + RecordingFormat = RecordingFormat.Mp4 + }); + return recordingResult.Value.RecordingId; + } + + private async Task StopRecordingAsync(LawyerVideoConferenceSchedule schedule) + { + if (string.IsNullOrEmpty(schedule.ServerCallID)) + { + Console.WriteLine("ServerCallID is not set for this transaction."); + return; + } + + if (string.IsNullOrEmpty(schedule.RecordingID)) + { + Console.WriteLine("Recording ID is not set for this transaction."); + return; + } + + await _callAutomationClient.GetCallRecording().StopAsync(schedule.RecordingID); + } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/EnotaryoPH.Web.csproj b/EnotaryoPH/EnotaryoPH.Web/EnotaryoPH.Web.csproj index 10e1fc5..0ecca6a 100644 --- a/EnotaryoPH/EnotaryoPH.Web/EnotaryoPH.Web.csproj +++ b/EnotaryoPH/EnotaryoPH.Web/EnotaryoPH.Web.csproj @@ -11,6 +11,7 @@ + diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Index.cshtml b/EnotaryoPH/EnotaryoPH.Web/Pages/Index.cshtml index f9ff8d4..40ddaa4 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Pages/Index.cshtml +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Index.cshtml @@ -8,3 +8,19 @@

Welcome

Learn about building Web apps with ASP.NET Core.

+ +
+ +
+ + + +@section Scripts { + +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Index.cshtml.cs b/EnotaryoPH/EnotaryoPH.Web/Pages/Index.cshtml.cs index 17a2547..8ae2c1e 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Pages/Index.cshtml.cs +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Index.cshtml.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; @@ -7,12 +6,14 @@ namespace EnotaryoPH.Web.Pages public class IndexModel : PageModel { private readonly ICurrentUserService _currentUserService; - private readonly NotificationService _notificationService; + private readonly INotificationService _notificationService; + private readonly IEventService _eventService; - public IndexModel(ICurrentUserService currentUserService, NotificationService notificationService) + public IndexModel(ICurrentUserService currentUserService, INotificationService notificationService, IEventService eventService) { _currentUserService = currentUserService; _notificationService = notificationService; + _eventService = eventService; } public IActionResult OnGet() @@ -34,5 +35,14 @@ namespace EnotaryoPH.Web.Pages return Page(); } + + public async Task OnPostAsync() + { + var message = new { Message = "the quick brown fox jumps over the lazy dog." }; + + await _eventService.LogAsync(NotaryoEvent.TransactionApproved, new Guid("0195dfd2-9048-77f1-9d9d-974828cb3e68")); + //await _notificationService.NotifyUserAsync("admin@enotaryo.ph", JsonSerializer.Serialize(message)); + return Redirect("/"); + } } } \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Notary/TransactionStatus/Index.cshtml.cs b/EnotaryoPH/EnotaryoPH.Web/Pages/Notary/TransactionStatus/Index.cshtml.cs index 504dd8f..95d0eff 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Pages/Notary/TransactionStatus/Index.cshtml.cs +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Notary/TransactionStatus/Index.cshtml.cs @@ -9,15 +9,15 @@ namespace EnotaryoPH.Web.Pages.Notary.TransactionStatus { private const string STATUS_READY = "Ready"; private readonly ICurrentUserService _currentUserService; - private readonly IConferenceSheduleService _conferenceSheduleService; private readonly NotaryoDBContext _notaryoDBContext; + private readonly IVideoConferenceService _videoConferenceService; private Transaction? _transaction; - public IndexModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IConferenceSheduleService conferenceSheduleService) + public IndexModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IVideoConferenceService videoConferenceService) { _notaryoDBContext = notaryoDBContext; _currentUserService = currentUserService; - _conferenceSheduleService = conferenceSheduleService; + _videoConferenceService = videoConferenceService; } public IActionResult OnGet() @@ -81,16 +81,12 @@ namespace EnotaryoPH.Web.Pages.Notary.TransactionStatus _transaction.Status = nameof(TransactionState.Accepted); _transaction.LawyerID = lawyer.LawyerID; _notaryoDBContext.Update(_transaction); + _notaryoDBContext.SaveChanges(); - var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID); - if (schedule_UID != Guid.Empty) - { - return _transaction.TransactionSignatories.TrueForAll(sig => sig.Status == nameof(SignatoryStatus.FaceMatch)) - ? Redirect($"/Participant/VideoCall/Room/{Transaction_UID}") - : Redirect($"/Participant/VideoCall/Waiting/{Transaction_UID}"); - } - - return Redirect("/Notary/Dashboard"); + var canStart = _videoConferenceService.CanStart(Transaction_UID); + return canStart + ? Redirect($"/Participant/VideoCall/Room/{Transaction_UID}") + : Redirect($"/Participant/VideoCall/Waiting/{Transaction_UID}"); } private string ChangeStatusLabel(string status) => status switch { nameof(SignatoryStatus.FaceMatch) => STATUS_READY, _ => status }; diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml index 28c507c..4248443 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml @@ -6,98 +6,305 @@ } @section Head { - + .modal-header.draggable { + cursor: move; + } + } - -
-
-
- 32:04 -
-
- - -
-
-
-
- - +
- - - + + + + +
+ + + + + + + + +@* + + + *@ @section Scripts { + } diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.cs b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.cs index d3d319a..2eda6a4 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.cs +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.cs @@ -7,18 +7,17 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall { public class RoomModel : PageModel { - private const int VideoConferenceExpirationInHours = 2; - private readonly IConferenceSheduleService _conferenceSheduleService; private readonly ICurrentUserService _currentUserService; private readonly NotaryoDBContext _dbContext; - private LawyerVideoConferenceSchedule _LawyerVideoConferenceSchedule; + private readonly IVideoConferenceService _videoConferenceService; private Transaction _Transaction; - public RoomModel(ICurrentUserService currentUserService, NotaryoDBContext dbContext, IConferenceSheduleService conferenceSheduleService) + public RoomModel(ICurrentUserService currentUserService, NotaryoDBContext dbContext, + IVideoConferenceService videoConferenceService) { _currentUserService = currentUserService; _dbContext = dbContext; - _conferenceSheduleService = conferenceSheduleService; + _videoConferenceService = videoConferenceService; } public async Task OnGetAsync() @@ -30,8 +29,8 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall } var currentUser = _dbContext.Users.Single(u => u.User_UID == _currentUserService.GetUser_UID()); - var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID); - if (schedule_UID == Guid.Empty) + var canStart = _videoConferenceService.CanStart(Transaction_UID); + if (!canStart) { if (_Transaction.PrincipalID == currentUser.UserID) { @@ -46,59 +45,54 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall return Redirect("/"); } - LoadVideoConferenceSchedule(schedule_UID); - - if ((DateTime.UtcNow - _LawyerVideoConferenceSchedule.MeetingDate).TotalHours > VideoConferenceExpirationInHours) + if (_videoConferenceService.HasExpired(Transaction_UID)) { - if (!_LawyerVideoConferenceSchedule.Status.IsInList(VideoConferenceStatus.Abandoned, VideoConferenceStatus.Completed)) - { - _LawyerVideoConferenceSchedule.Status = nameof(VideoConferenceStatus.Expired); - _dbContext.Update(_LawyerVideoConferenceSchedule); - _dbContext.SaveChanges(); - } - TempData["Warning"] = "The video conference has expired."; return Redirect("/"); } + var schedule_UID = await _videoConferenceService.StartAsync(Transaction_UID); + CommunicationUserToken = currentUser.Role == nameof(UserType.Notary) - ? _LawyerVideoConferenceSchedule.MeetingRoomTokenID - : _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID).MeetingRoomTokenID; - CommunicationRoomId = _LawyerVideoConferenceSchedule.MeetingRoomID; + ? _Transaction.Schedule.MeetingRoomTokenID + : GetParticipant(currentUser).MeetingRoomTokenID; + CommunicationRoomId = _Transaction.Schedule.MeetingRoomID; CommunicationUserId = currentUser.Role == nameof(UserType.Notary) - ? _LawyerVideoConferenceSchedule.MeetingRoomUserID - : _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID).MeetingRoomUserID; + ? _Transaction.Schedule.MeetingRoomUserID + : GetParticipant(currentUser).MeetingRoomUserID; + DisplayName = currentUser.Role == nameof(UserType.Notary) + ? _Transaction.Lawyer.User.Fullname + : GetParticipant(currentUser).Participant.Fullname.DefaultIfEmpty(currentUser.Email); + ParticipantType = currentUser.Role == nameof(UserType.Notary) + ? nameof(UserType.Notary) + : GetParticipant(currentUser).Participant.Role; return Page(); } - public async Task OnPostStartRecordingAsync() + private LawyerVideoConferenceParticipant GetParticipant(User currentUser) => _Transaction.Schedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID); + + public async Task OnPostApproveAsync() { - LoadTransaction(); - var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID); - LoadVideoConferenceSchedule(schedule_UID); - await _conferenceSheduleService.StartRecordingAsync(Transaction_UID, ServerCallID); + await _videoConferenceService.ApproveTransactionAsync(Transaction_UID); + return new JsonResult(true); } - public async Task OnPostStopRecordingAsync() + public async Task OnPostStartRecordingAsync() { LoadTransaction(); - var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID); - LoadVideoConferenceSchedule(schedule_UID); - await _conferenceSheduleService.StopRecordingAsync(Transaction_UID); + await _videoConferenceService.StartRecordingAsync(Transaction_UID, ServerCallID); + return new JsonResult(true); } private void LoadTransaction() => _Transaction = _dbContext.Transactions - .Include(t => t.TransactionSignatories) - .Include(t => t.Principal) - .FirstOrDefault(t => t.Transaction_UID == Transaction_UID); - - private void LoadVideoConferenceSchedule(Guid schedule_UID) => _LawyerVideoConferenceSchedule = _dbContext.LawyerVideoConferenceSchedules - .Include(sched => sched.LawyerVideoConferenceParticipants) - .ThenInclude(p => p.Participant) - .Include(sched => sched.Lawyer) - .ThenInclude(l => l.User) - .FirstOrDefault(sched => sched.LawyerVideoConferenceSchedule_UID == schedule_UID); + .Include(t => t.TransactionSignatories) + .Include(t => t.Lawyer) + .ThenInclude(l => l.User) + .Include(t => t.Principal) + .Include(t => t.Schedule) + .ThenInclude(sch => sch.LawyerVideoConferenceParticipants) + .FirstOrDefault(t => t.Transaction_UID == Transaction_UID); public string CommunicationRoomId { get; private set; } @@ -106,25 +100,33 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall public string CommunicationUserToken { get; private set; } + public string DisplayName { get; private set; } + + public string ParticipantType { get; set; } + public List Participants { get { - var signatoryTypes = _Transaction.TransactionSignatories.Where(t => t.UserID > 0).ToDictionary(k => k.UserID, v => v.Type); - var participants = _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.ConvertAll(p => new RoomParticipantViewModel - { - Id = p.LawyerVideoConferenceParticipant_UID.ToString(), - DisplayName = $"{p.Participant.Firstname} {p.Participant.Lastname}".Trim().DefaultIfEmpty(p.Participant.Email), - RoomUserID = p.MeetingRoomUserID, - Type = signatoryTypes.GetValueOrDefault(p.ParticipantID, nameof(UserType.Principal)) - }); + var signatoryTypes = _Transaction.TransactionSignatories.Where(t => t.UserID > 0) + .ToDictionary(k => k.UserID, v => v.Type); + var participants = _Transaction.Schedule.LawyerVideoConferenceParticipants.ConvertAll(p => + new RoomParticipantViewModel + { + Id = p.LawyerVideoConferenceParticipant_UID.ToString(), + DisplayName = + $"{p.Participant.Firstname} {p.Participant.Lastname}".Trim() + .DefaultIfEmpty(p.Participant.Email), + RoomUserID = p.MeetingRoomUserID, + Type = signatoryTypes.GetValueOrDefault(p.ParticipantID, nameof(UserType.Principal)) + }); - var host = _LawyerVideoConferenceSchedule.Lawyer.User; + var host = _Transaction.Schedule.Lawyer.User; participants.Add(new RoomParticipantViewModel { DisplayName = $"{host.Firstname} {host.Lastname}".Trim().DefaultIfEmpty(host.Email), Id = Guid.Empty.ToString(), - RoomUserID = _LawyerVideoConferenceSchedule.MeetingRoomUserID, + RoomUserID = _Transaction.Schedule.MeetingRoomUserID, Type = nameof(UserType.Notary) }); @@ -132,10 +134,8 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall } } - [BindProperty(SupportsGet = true)] - public string ServerCallID { get; set; } + [BindProperty(SupportsGet = true)] public string ServerCallID { get; set; } - [BindProperty(SupportsGet = true)] - public Guid Transaction_UID { get; set; } + [BindProperty(SupportsGet = true)] public Guid Transaction_UID { get; set; } } } \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.js b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.js index 79ec01f..a7e918e 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.js +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.js @@ -5,114 +5,291 @@ control_communicationRoomId = document.getElementById("CommunicationRoomId"), control_communicationUserId = document.getElementById("CommunicationUserId"), control_communicationUserToken = document.getElementById("CommunicationUserToken"), + control_displayName = document.getElementById("DisplayName"), control_participants = document.getElementById("Participants"), control_reject = document.getElementById("Reject"), - control_templateVideo = document.getElementById("templateVideo"), - control_videoGrid = document.getElementById("VideoGrid"), + control_templateSidePane = document.getElementById("TemplateSidePane"), + control_templateParticipantItem = document.getElementById("TemplateParticipantItem"), + control_draggableModal = document.getElementById("DraggableModal"), + control_rightSidebarModal = document.getElementById("RightSidebarModal"), + /*control_videoGrid = document.getElementById("VideoGrid"),*/ control_videoGridContainer = document.getElementById("videoGrid-container"), control_viewDocument = document.getElementById("ViewDocument"), control_serverCallIID = document.getElementById("ServerCallID"), + control_participantType = document.getElementById("ParticipantType"), + control_participantListGroup = document.getElementById("ParticipantListGroup"), x = 1; let participants = JSON.parse(control_participants.value); async function _initVideoCall() { - let userAccessToken = control_communicationUserToken.value; + let self = this; + let customButtons = []; + if (control_participantType.value == 'Notary') { + customButtons = [ + function (args) { + return { + placement: 'secondary', + // Icon registered by the composites. + iconName: 'OpenAttachment', + strings: { + label: 'View Document', + tooltipContent: 'View the document.' + }, + onItemClick: function () { + alert('Document Modal goes here.'); + } + }; + }, + function (args) { + return { + placement: 'secondary', + // Icon registered by the composites. + iconName: 'ControlButtonParticipantsContextualMenuItem', + strings: { + label: 'View Participants', + tooltipContent: 'View the participants.' + }, + onItemClick: function () { + const modal = bootstrap.Modal.getOrCreateInstance(control_rightSidebarModal); + modal.show(); + } + }; + }, + function (args, b, c) { + return { + placement: 'primary', + // Icon registered by the composites. + iconName: 'EditBoxSubmit', + strings: { + label: 'Approve', + tooltipContent: 'Approve the Transaction' + }, + onItemClick: approveTransaction + }; + }, + function (args, b, c) { + return { + placement: 'primary', + // Icon registered by the composites. + iconName: 'EditBoxCancel', + strings: { + label: 'Reject', + tooltipContent: 'Reject the Transaction' + }, + onItemClick: function () { + alert('Rejected!'); + } + }; + } + ]; + } else { + customButtons = [ + function (args) { + return { + placement: 'primary', + // Icon registered by the composites. + iconName: 'OpenAttachment', + strings: { + label: 'View Document', + tooltipContent: 'View the document.' + }, + onItemClick: function () { + alert('Document Modal goes here.'); + } + }; + }, + function (args) { + return { + placement: 'secondary', + // Icon registered by the composites. + iconName: 'OpenAttachment', + strings: { + label: 'View Document', + tooltipContent: 'View the document.' + }, + onItemClick: function () { + alert('Document Modal goes here.'); + } + }; + }, + ]; + } + const videoCall = jfa.communication.videocall; let options = { - onCreateLocalVideoStream: function (el) { - if (el) { - el.style['transform'] = ''; - let notary = participants.find(p => p.RoomUserID == control_communicationUserId.value); - document.getElementById(notary.Id).appendChild(el); - } + //onCreateLocalVideoStream: function (el) { + // if (el) { + // el.style['transform'] = ''; + // let notary = participants.find(p => p.RoomUserID == control_communicationUserId.value); + // document.getElementById(notary.Id).appendChild(el); + // } + //}, + //remoteVideoIsAvailableChanged: function (e) { + // let participant = participants.find(p => p.RoomUserID == e.participantId); + // if (participant) { + // e.el.querySelector('video').style['object-fit'] = 'cover'; + // document.getElementById(participant.Id).appendChild(e.el); + // } + //}, + onGetServerCallIDCallback: function (serverCallId) { + // let url = jfa.utilities.routing.getCurrentURLWithHandler("StartRecording"); + // url.searchParams.append("ServerCallID", serverCallId); + // jfa.utilities.request.post(url, {}) + // .then(resp => { + // if (resp.ok === true) { + // console.log("started recording"); + // } + // }) + // .catch(err => console.error(err)); + //debugger; }, - remoteVideoIsAvailableChanged: function (e) { - let participant = participants.find(p => p.RoomUserID == e.participantId); - if (participant) { - e.el.querySelector('video').style['object-fit'] = 'cover'; - document.getElementById(participant.Id).appendChild(e.el); - } + stateChangedCallback: function (state) { + //console.log(state); }, - onGetServerCallID: function (serverCallId) { - let url = jfa.utilities.routing.getCurrentURLWithHandler("StartRecording"); - url.searchParams.append("ServerCallID", serverCallId); - jfa.utilities.request.post(url, {}) - .then(resp => { - if (resp.ok === true) { - console.log("started recording"); - } - }) - .catch(err => console.error(err)); - } + onFetchParticipantMenuItemsCallback: function (participantUserId, userId, defaultMenuItems) { + const participantList = document.querySelector('[data-ui-id="people-pane-content"]'); + if (participantList) { + participantList.innerHTML = ''; + console.log(participants); + } + defaultMenuItems = []; + }, + onFetchCustomButtonPropsCallbacks: customButtons, + onAddParticipantCallback: function (e, f) { + //debugger; + console.log(e, f); + }, + participantsJoinedCallback: function (e, f) { + //debugger; + console.log(e, f); + }, + callEndedCallback: function (a, b, c) { + }, + token: control_communicationUserToken.value, + roomID: control_communicationRoomId.value, + displayName: control_displayName.value, + userID: control_communicationUserId.value, + videoContainer: control_videoGridContainer }; - await videoCall.init(userAccessToken, options); - videoCall.joinRoom(control_communicationRoomId.value); + await videoCall.init(options); + await videoCall.joinRoom(); } - function _updateGrid() { - control_videoGrid.innerHTML = ''; + function approveTransaction(e) { + jfa.communication.videocall.stopCall(true); + let url = jfa.utilities.routing.getCurrentURLWithHandler("Approve"); + jfa.utilities.request.post(url, {}) + .then(resp => { + if (resp.ok === true) { + debugger; + } + }) + .catch(err => console.error(err)); + } - participants.forEach((participant, index) => { - const col = document.createElement('div'); - col.className = 'participant-col'; - const tmpl = control_templateVideo.cloneNode(true).content; - const vidcontainer = tmpl.querySelector(".video-container") - if (vidcontainer) { - vidcontainer.id = participant.Id; - vidcontainer.classList.add(participant.Id); - vidcontainer.classList.add(participant.Type == 'Notary' ? 'local-video-container' : 'remote-video-container'); + //function _updateGrid() { + // control_videoGrid.innerHTML = ''; + + // participants.forEach((participant, index) => { + // const col = document.createElement('div'); + // col.className = 'participant-col'; + // const tmpl = control_templateVideo.cloneNode(true).content; + // const vidcontainer = tmpl.querySelector(".video-container") + // if (vidcontainer) { + // vidcontainer.id = participant.Id; + // vidcontainer.classList.add(participant.Id); + // vidcontainer.classList.add(participant.Type == 'Notary' ? 'local-video-container' : 'remote-video-container'); + // } + // let participantName = tmpl.querySelector(".participant-name") + // if (participantName) { + // participantName.textContent = participant.DisplayName; + // } + // col.appendChild(tmpl); + // control_videoGrid.appendChild(col); + // }); + + // // Dynamically adjust grid columns based on participant count + // const count = participants.length; + // const cols = count <= 2 ? 'col-12 col-sm-12 offset-md-1 col-md-10 offset-lg-0 col-lg-6' : + // count <= 4 ? 'col-6 col-sm-6 col-md-6 col-lg-6 col-xl-6' : + // count <= 8 ? 'col-6 col-md-6 col-lg-4 col-xl-4' : + // count <= 9 ? 'col-4' : + // 'col-6 col-sm-4 col-lg-3'; + + // document.querySelectorAll('.participant-col').forEach(el => { + // el.className = `participant-col ${cols}`; + // }); + + // const fluid = count <= 2 ? 'container-fluid' : + // count <= 8 ? 'container-xxl' : + // 'container-xl' + // control_videoGridContainer.className = `${fluid} py-3`; + //} + + function _createParticipantListItems() { + control_participantListGroup.innerHTML = ''; + participants.forEach(participant => { + if (participant.Type === 'Notary') { + return; } - let participantName = tmpl.querySelector(".participant-name") - if (participantName) { - participantName.textContent = participant.DisplayName; - } - col.appendChild(tmpl); - control_videoGrid.appendChild(col); + const initials = participant.DisplayName.split(' ').map(n => n.charAt(0)).join(''); + let tmpl = control_templateParticipantItem.cloneNode(true).content; + tmpl.querySelector('.participant-item').dataset.participantUid = participant.RoomUserID; + tmpl.querySelector('.participant-avatar').textContent = initials; + tmpl.querySelector('.participant-name').textContent = participant.DisplayName; + control_participantListGroup.appendChild(tmpl); }); - - // Dynamically adjust grid columns based on participant count - const count = participants.length; - const cols = count <= 2 ? 'col-12 col-sm-12 offset-md-1 col-md-10 offset-lg-0 col-lg-6' : - count <= 4 ? 'col-6 col-sm-6 col-md-6 col-lg-6 col-xl-6' : - count <= 8 ? 'col-6 col-md-6 col-lg-4 col-xl-4' : - count <= 9 ? 'col-4' : - 'col-6 col-sm-4 col-lg-3'; - - document.querySelectorAll('.participant-col').forEach(el => { - el.className = `participant-col ${cols}`; - }); - - const fluid = count <= 2 ? 'container-fluid' : - count <= 8 ? 'container-xxl' : - 'container-xl' - control_videoGridContainer.className = `${fluid} py-3`; } function _bindEvents() { - window.addEventListener('resize', _updateGrid); - control_viewDocument.addEventListener("click", function () { - alert('not yet implemented'); + control_participantListGroup.addEventListener('click', function (event) { + let target = event.target?.closest('.list-group-item'); + if (target) { + + const sidebarModal = bootstrap.Modal.getOrCreateInstance(control_rightSidebarModal); + sidebarModal.hide(); + + const draggableModal = bootstrap.Modal.getOrCreateInstance(control_draggableModal); + draggableModal.show(); + } }); - control_approve.addEventListener("click", function () { - let url = jfa.utilities.routing.getCurrentURLWithHandler("StopRecording"); - jfa.utilities.request.post(url, {}) - .then(resp => { - if (resp.ok === true) { - console.log("stopped recording"); - } - }) - .catch(err => console.error(err)); - }); - control_reject.addEventListener("click", function () { - alert('not yet implemented'); + + control_draggableModal.addEventListener('shown.bs.modal', function (e) { + makeDraggable(control_draggableModal); }); } + function makeDraggable(modal) { + let isDragging = false; + let offsetX, offsetY; + + const header = modal.querySelector('.modal-header'); + + header.addEventListener('mousedown', (e) => { + isDragging = true; + offsetX = e.clientX - modal.offsetLeft; + offsetY = e.clientY - modal.offsetTop; + }); + + document.addEventListener('mousemove', (e) => { + if (!isDragging) return; + modal.style.left = `${e.clientX - offsetX}px`; + modal.style.top = `${e.clientY - offsetY}px`; + }); + + document.addEventListener('mouseup', () => { + isDragging = false; + }); + } + + async function _init() { _bindEvents(); - _updateGrid(); + //_updateGrid(); await _initVideoCall(); + debugger; + _createParticipantListItems(); } await _init(); diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Waiting.cshtml.cs b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Waiting.cshtml.cs index cc8699b..ed44390 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Waiting.cshtml.cs +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Waiting.cshtml.cs @@ -9,32 +9,31 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall { private readonly NotaryoDBContext _dbContext; private readonly ICurrentUserService _currentUserService; - private readonly IConferenceSheduleService _conferenceSheduleService; + private readonly IVideoConferenceService _videoConferenceService; private Transaction _transactionEntity; - public WaitingModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IConferenceSheduleService conferenceSheduleService) + public WaitingModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IVideoConferenceService videoConferenceService) { _dbContext = notaryoDBContext; _currentUserService = currentUserService; - _conferenceSheduleService = conferenceSheduleService; + _videoConferenceService = videoConferenceService; } public async Task OnGetAsync() { _transactionEntity = _dbContext.Transactions - .Include(t => t.TransactionSignatories) - .ThenInclude(ts => ts.User) - .Include(t => t.Lawyer) - .ThenInclude(l => l.User) - .Include(t => t.Principal) - .FirstOrDefault(t => t.Transaction_UID == Transaction_UID); + .Include(t => t.TransactionSignatories) + .ThenInclude(ts => ts.User) + .Include(t => t.Lawyer) + .ThenInclude(l => l.User) + .Include(t => t.Principal) + .FirstOrDefault(t => t.Transaction_UID == Transaction_UID); if (_transactionEntity == null || _transactionEntity.Status.IsInList(TransactionState.Completed)) { return NotFound(); } - var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID); - if (schedule_UID != Guid.Empty) + if (_videoConferenceService.CanStart(Transaction_UID)) { return Redirect($"/Participant/VideoCall/Room/{Transaction_UID}"); } diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Shared/_Blank.cshtml b/EnotaryoPH/EnotaryoPH.Web/Pages/Shared/_Blank.cshtml index 7cbca5d..c3d0f6c 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Pages/Shared/_Blank.cshtml +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Shared/_Blank.cshtml @@ -71,7 +71,8 @@ .withUrl("/notificationHub") .build(); - connection.on("ReceiveUserNotification", (user_UID, message) => { + connection.on("ReceiveUserNotification", (userID, message) => { + alert(message); receiveUserNotificationCallback?.(message); }); diff --git a/EnotaryoPH/EnotaryoPH.Web/Program.cs b/EnotaryoPH/EnotaryoPH.Web/Program.cs index 52f7435..14a3838 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Program.cs +++ b/EnotaryoPH/EnotaryoPH.Web/Program.cs @@ -2,6 +2,7 @@ using System.Security.Principal; using Azure.Communication.CallAutomation; using Azure.Communication.Identity; using Azure.Communication.Rooms; +using Azure.Storage.Queues; using Coravel; using EnotaryoPH.Data; using EnotaryoPH.Web.Common.Hubs; @@ -35,7 +36,7 @@ namespace EnotaryoPH.Web razorBuilder.AddRazorRuntimeCompilation(); #endif builder.Services.AddSignalR(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); var config = builder.Configuration; builder.Services.AddTransient(provider => config); @@ -60,9 +61,12 @@ namespace EnotaryoPH.Web return new CompreFaceClient(host, port); }); builder.Services.AddQueue(); + builder.Services.AddScheduler(); builder.Services.AddMailer(config); builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddScoped(provider => { var connectionString = config.GetConnectionString("AzureCommunication"); @@ -84,9 +88,20 @@ namespace EnotaryoPH.Web ? throw new InvalidConfigurationException("AzureCommunication", string.Empty) : new CallAutomationClient(connectionString); }); + builder.Services.AddScoped(provider => + { + var connectionString = config.GetConnectionString("AzureStorage"); + return string.IsNullOrEmpty(connectionString) + ? throw new InvalidConfigurationException("AzureStorage", string.Empty) + : new QueueClient(connectionString, "recording-ready"); + }); var app = builder.Build(); + app.Services.UseScheduler(scheduler => scheduler + .Schedule() + .EveryTenSeconds()); + // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { diff --git a/EnotaryoPH/EnotaryoPH.Web/package.json b/EnotaryoPH/EnotaryoPH.Web/package.json index 202cd5e..40ea39b 100644 --- a/EnotaryoPH/EnotaryoPH.Web/package.json +++ b/EnotaryoPH/EnotaryoPH.Web/package.json @@ -9,8 +9,8 @@ "context": "browser", "outputFormat": "global", "scopeHoist": false, - "optimize": true, - "sourceMap": false + "optimize": false, + "sourceMap": true } }, "browserslist": "> 0.5%", diff --git a/EnotaryoPH/EnotaryoPH.Web/wwwroot/lib/azure-communication-service/callComposite.js b/EnotaryoPH/EnotaryoPH.Web/wwwroot/lib/azure-communication-service/callComposite.js new file mode 100644 index 0000000..9e29078 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/wwwroot/lib/azure-communication-service/callComposite.js @@ -0,0 +1,2 @@ +/*! For license information please see callComposite.js.LICENSE.txt */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.callComposite=t():e.callComposite=t()}(self,(()=>(()=>{var e,t,i={2222:(e,t,i)=>{"use strict";var n=i(2900),r={"text/plain":"Text","text/html":"Url",default:"Text"};e.exports=function(e,t){var i,s,a,o,l,c,d=!1;t||(t={}),i=t.debug||!1;try{if(a=n(),o=document.createRange(),l=document.getSelection(),(c=document.createElement("span")).textContent=e,c.ariaHidden="true",c.style.all="unset",c.style.position="fixed",c.style.top=0,c.style.clip="rect(0, 0, 0, 0)",c.style.whiteSpace="pre",c.style.webkitUserSelect="text",c.style.MozUserSelect="text",c.style.msUserSelect="text",c.style.userSelect="text",c.addEventListener("copy",(function(n){if(n.stopPropagation(),t.format)if(n.preventDefault(),void 0===n.clipboardData){i&&console.warn("unable to use e.clipboardData"),i&&console.warn("trying IE specific stuff"),window.clipboardData.clearData();var s=r[t.format]||r.default;window.clipboardData.setData(s,e)}else n.clipboardData.clearData(),n.clipboardData.setData(t.format,e);t.onCopy&&(n.preventDefault(),t.onCopy(n.clipboardData))})),document.body.appendChild(c),o.selectNodeContents(c),l.addRange(o),!document.execCommand("copy"))throw new Error("copy command was unsuccessful");d=!0}catch(n){i&&console.error("unable to copy using execCommand: ",n),i&&console.warn("trying IE specific stuff");try{window.clipboardData.setData(t.format||"text",e),t.onCopy&&t.onCopy(window.clipboardData),d=!0}catch(n){i&&console.error("unable to copy using clipboardData: ",n),i&&console.error("falling back to prompt"),s=function(e){var t=(/mac os x/i.test(navigator.userAgent)?"⌘":"Ctrl")+"+C";return e.replace(/#{\s*key\s*}/g,t)}("message"in t?t.message:"Copy to clipboard: #{key}, Enter"),window.prompt(s,e)}}finally{l&&("function"==typeof l.removeRange?l.removeRange(o):l.removeAllRanges()),c&&document.body.removeChild(c),a()}return d}},8156:e=>{"use strict";var t,i="object"==typeof Reflect?Reflect:null,n=i&&"function"==typeof i.apply?i.apply:function(e,t,i){return Function.prototype.apply.call(e,t,i)};t=i&&"function"==typeof i.ownKeys?i.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var r=Number.isNaN||function(e){return e!=e};function s(){s.init.call(this)}e.exports=s,e.exports.once=function(e,t){return new Promise((function(i,n){function r(i){e.removeListener(t,s),n(i)}function s(){"function"==typeof e.removeListener&&e.removeListener("error",r),i([].slice.call(arguments))}m(e,t,s,{once:!0}),"error"!==t&&function(e,t){"function"==typeof e.on&&m(e,"error",t,{once:!0})}(e,r)}))},s.EventEmitter=s,s.prototype._events=void 0,s.prototype._eventsCount=0,s.prototype._maxListeners=void 0;var a=10;function o(e){if("function"!=typeof e)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof e)}function l(e){return void 0===e._maxListeners?s.defaultMaxListeners:e._maxListeners}function c(e,t,i,n){var r,s,a,c;if(o(i),void 0===(s=e._events)?(s=e._events=Object.create(null),e._eventsCount=0):(void 0!==s.newListener&&(e.emit("newListener",t,i.listener?i.listener:i),s=e._events),a=s[t]),void 0===a)a=s[t]=i,++e._eventsCount;else if("function"==typeof a?a=s[t]=n?[i,a]:[a,i]:n?a.unshift(i):a.push(i),(r=l(e))>0&&a.length>r&&!a.warned){a.warned=!0;var d=new Error("Possible EventEmitter memory leak detected. "+a.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");d.name="MaxListenersExceededWarning",d.emitter=e,d.type=t,d.count=a.length,c=d,console&&console.warn&&console.warn(c)}return e}function d(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function u(e,t,i){var n={fired:!1,wrapFn:void 0,target:e,type:t,listener:i},r=d.bind(n);return r.listener=i,n.wrapFn=r,r}function h(e,t,i){var n=e._events;if(void 0===n)return[];var r=n[t];return void 0===r?[]:"function"==typeof r?i?[r.listener||r]:[r]:i?function(e){for(var t=new Array(e.length),i=0;i0&&(a=t[0]),a instanceof Error)throw a;var o=new Error("Unhandled error."+(a?" ("+a.message+")":""));throw o.context=a,o}var l=s[e];if(void 0===l)return!1;if("function"==typeof l)n(l,this,t);else{var c=l.length,d=p(l,c);for(i=0;i=0;s--)if(i[s]===t||i[s].listener===t){a=i[s].listener,r=s;break}if(r<0)return this;0===r?i.shift():function(e,t){for(;t+1=0;n--)this.removeListener(e,t[n]);return this},s.prototype.listeners=function(e){return h(this,e,!0)},s.prototype.rawListeners=function(e){return h(this,e,!1)},s.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):g.call(e,t)},s.prototype.listenerCount=g,s.prototype.eventNames=function(){return this._eventsCount>0?t(this._events):[]}},7669:(e,t,i)=>{"use strict";var n=i(538),r=i(2300);function s(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,i=1;i