diff --git a/api/QCVOC.Api/Scans/Controller/ScansController.cs b/api/QCVOC.Api/Scans/Controller/ScansController.cs index 31bc1328..2e9c4d97 100644 --- a/api/QCVOC.Api/Scans/Controller/ScansController.cs +++ b/api/QCVOC.Api/Scans/Controller/ScansController.cs @@ -48,6 +48,63 @@ public ScansController(ITripleKeyRepository scanRepository, ISingleKeyRepo private ISingleKeyRepository VeteranRepository { get; set; } private ISingleKeyRepository ServiceRepository { get; set; } + /// + /// Returns a check in scan for the specified Veteran. + /// + /// The Id of the Event. + /// Either the Veteran Id or Card Number of the Veteran. + /// See attributes. + /// The Scan was retrieved successfully. + /// The Veteran Id or Card Number is invalid. + /// Unauthorized. + /// The specified Card Number or Veteran Id doesn't match an enrolled Veteran, or the Veteran has not checked in to the specified event. + /// The server encountered an error while processing the request. + [HttpGet("{eventId}/{id}/checkin")] + [Authorize] + [ProducesResponseType(typeof(Scan), 200)] + [ProducesResponseType(400)] + [ProducesResponseType(401)] + [ProducesResponseType(404)] + [ProducesResponseType(typeof(Exception), 500)] + public IActionResult GetCheckIn(Guid eventId, string id) + { + if (string.IsNullOrEmpty(id)) + { + return BadRequest($"The card or veteran ID is null or empty."); + } + + var veteran = default(Veteran); + + if (int.TryParse(id, out var cardNumber)) + { + veteran = VeteranRepository + .GetAll(new VeteranFilters() { CardNumber = cardNumber }) + .SingleOrDefault(); + } + else if (Guid.TryParse(id, out var veteranId)) + { + veteran = VeteranRepository.Get(veteranId); + } + else + { + return BadRequest($"The provided ID is neither a Card Number nor Veteran ID."); + } + + if (veteran == default(Veteran)) + { + return StatusCode(404, $"The specified Card Number or Veteran Id doesn't match an enrolled Veteran."); + } + + var scan = ScanRepository.Get(eventId, veteran.Id, Guid.Empty); + + if (scan == default(Scan)) + { + return StatusCode(404, $"The Veteran has not checked in for this Event."); + } + + return Ok(scan); + } + /// /// Returns a list of Scans. /// @@ -85,28 +142,28 @@ public IActionResult GetAll([FromQuery]ScanFilters filters) [ProducesResponseType(typeof(Exception), 500)] public IActionResult Delete(Guid eventId, string id) { - return Delete(eventId, id, null); + return Delete(eventId, Guid.Empty, id); } /// /// Deletes a Service Scan. /// /// The Id of the Event. + /// The Id of the Service. /// Either the Veteran Id or Card Number of the Veteran. - /// The optional Service Id. /// See attributes. /// The Scan was deleted successfully. /// The Veteran Id or Card Number is invalid. /// Unauthorized. /// The specified Card Number or Veteran Id doesn't match an enrolled Veteran. /// The server encountered an error while processing the request. - [HttpDelete("{eventId}/{id}/{serviceId}")] + [HttpDelete("{eventId}/{serviceId}/{id}")] [Authorize] [ProducesResponseType(400)] [ProducesResponseType(401)] [ProducesResponseType(404)] [ProducesResponseType(typeof(Exception), 500)] - public IActionResult Delete(Guid eventId, string id, Guid? serviceId = null) + public IActionResult Delete(Guid eventId, Guid serviceId, string id) { if (string.IsNullOrEmpty(id)) { @@ -161,7 +218,7 @@ public IActionResult Delete(Guid eventId, string id, Guid? serviceId = null) /// The Scan was recorded or updated. /// The specified Scan was invalid. /// Unauthorized. - /// The Veteran has not checked in for the Event. + /// The Veteran has not checked in for the Event, or did not check in with a guest and is attempting to use a service with a guest. /// The specified Veteran, Event or Service was invalid. /// The Scan has already been recorded. /// The server encountered an error while processing the request. @@ -219,18 +276,17 @@ public IActionResult Scan([FromBody]ScanRequest scan) ServiceId = scan.ServiceId, ScanById = User.GetId(), ScanDate = DateTime.UtcNow, + PlusOne = scan.PlusOne, }; // check in scan if (scan.ServiceId == Guid.Empty) { - scanRecord.PlusOne = scan.PlusOne; - if (existingCheckIn == default(Scan)) { return CreateScan(scanRecord, veteran); } - else if (existingCheckIn.PlusOne != scan.PlusOne) + else if (existingCheckIn.PlusOne != scanRecord.PlusOne) { return UpdateScan(scanRecord, veteran); } @@ -246,6 +302,11 @@ public IActionResult Scan([FromBody]ScanRequest scan) return StatusCode(403, new ScanError(scanRecord, veteran, "The Veteran has not checked in for this Event.")); } + if (scanRecord.PlusOne && !existingCheckIn.PlusOne) + { + return StatusCode(403, new ScanError(scanRecord, veteran, "The Veteran did not check in with a +1.")); + } + var previousServiceScan = previousScans.Where(s => s.ServiceId == scan.ServiceId).SingleOrDefault(); if (previousServiceScan != default(Scan)) @@ -253,7 +314,6 @@ public IActionResult Scan([FromBody]ScanRequest scan) return Conflict(new ScanError(previousServiceScan, veteran, "Duplicate Scan")); } - scanRecord.PlusOne = existingCheckIn.PlusOne; return CreateScan(scanRecord, veteran); } diff --git a/web/src/scans/Scanner.js b/web/src/scans/Scanner.js index 2225e60a..287b3bd9 100644 --- a/web/src/scans/Scanner.js +++ b/web/src/scans/Scanner.js @@ -86,6 +86,7 @@ const initialState = { scanner: { event: undefined, service: undefined, + history: [], }, scan: { cardNumber: undefined, @@ -95,7 +96,6 @@ const initialState = { }, events: [], services: [], - history: [], plusOne: undefined, historyDialog: { open: false, @@ -105,6 +105,7 @@ const initialState = { }, plusOneDialog: { open: false, + intent: undefined, }, }; @@ -114,7 +115,24 @@ class Scanner extends Component { componentDidMount = () => { window.inputBarcodeScanner = this.handleBarcodeScanned; - this.fetchEvents('refreshApi'); + let scanner = undefined; + + try { + scanner = JSON.parse(sessionStorage.getItem('scanner')); + + if (scanner === undefined || scanner.event === undefined || scanner.service === undefined || scanner.history === undefined) { + throw scanner; + } + } catch { + sessionStorage.removeItem('scanner'); + } + + if (scanner && scanner.event && scanner.service) { + this.setState({ scanner: scanner }); + } + else { + this.fetchEvents('refreshApi'); + } } handleBarcodeScanned = (barcode) => { @@ -123,6 +141,35 @@ class Scanner extends Component { let { event, service } = this.state.scanner; let scan = { eventId: event && event.id, serviceId: service && service.id, cardNumber: barcode, plusOne: this.state.plusOne === undefined ? false : this.state.plusOne }; + if (scan.serviceId !== CHECKIN_SERVICE_ID) { + this.setState({ + scan: scan, + scanApi: { ...this.state.scanApi, isExecuting: true }, + }, () => { + this.props.context.api.get('/v1/scans/' + scan.eventId + '/' + scan.cardNumber + '/checkin') + .then(response => { + if (response.data.plusOne) { + this.setState({ + scanApi: { isExecuting: false, isErrored: false }, + plusOneDialog: { open: true, intent: 'service' }, + }); + } + else { + this.sendScan(scan, barcode); + } + }, error => { + this.setState({ scanApi: { isExecuting: false, isErrored: true }}, () => { + this.handleScanResponse(barcode, error.response); + }); + }); + }); + } + else { + this.sendScan(scan, barcode); + } + } + + sendScan = (scan, barcode) => { this.setState({ scan: initialState.scan, scanDialog: { open: false }, @@ -143,7 +190,7 @@ class Scanner extends Component { handleScanClick = () => { if (this.state.scanner.service && this.state.scanner.service.id === CHECKIN_SERVICE_ID) { - this.setState({ plusOneDialog: { open: true }}); + this.setState({ plusOneDialog: { open: true, intent: 'checkin' }}); } else { this.scan(); @@ -168,9 +215,22 @@ class Scanner extends Component { } handlePlusOneDialogClose = (result) => { - this.setState({ plusOneDialog: { open: false }, plusOne: result === undefined ? false : result }, () => { + let intent = this.state.plusOneDialog.intent; + + this.setState({ + plusOneDialog: { open: false, intent: undefined }, + plusOne: result === undefined ? false : result, + }, () => { if (result !== undefined) { - this.scan(); + if (intent === 'checkin') { + // for checkin scans, +1 selection comes before card entry + this.scan(); + } + else if (intent === 'service') { + // for service scans, +1 selection comes after card entry, and only if the veteran + // checked in with a +1. + this.sendScan({ ...this.state.scan, plusOne: result }); + } } }); } @@ -178,18 +238,27 @@ class Scanner extends Component { handleScanResponse = (cardNumber, response) => { let scan = { cardNumber: cardNumber, status: response.status, response: response.data }; - let history = this.state.history.slice(0); - history.unshift(scan); + let historyScan = JSON.parse(JSON.stringify(scan)); + if (historyScan && historyScan.response && historyScan.response.veteran && historyScan.response.veteran.photoBase64) { + delete historyScan.response.veteran.photoBase64; + } + + let history = this.state.scanner.history.slice(0); + history.unshift(historyScan); history = history.slice(0, historyLimit); this.setState({ scan: scan, - history: history, + scanner: { ...this.state.scanner, history: history }, + }, () => { + sessionStorage.setItem('scanner', JSON.stringify(this.state.scanner)); }); } resetScanner = (resolve) => { this.setState({ ...initialState }, () => { + sessionStorage.removeItem('scanner'); + this.fetchEvents('refreshApi') .then(() => resolve()); }); @@ -201,8 +270,10 @@ class Scanner extends Component { deleteScan = (scan) => { this.setState({ - history: this.state.history.filter(oldScan => oldScan.cardNumber !== scan.cardNumber), + scanner: { ...this.state.scanner, history: this.state.scanner.history.filter(oldScan => oldScan.cardNumber !== scan.cardNumber) }, }, () => { + sessionStorage.setItem('scanner', JSON.stringify(this.state.scanner)); + if (this.state.scan.cardNumber === scan.cardNumber) { this.clearLastScan(); } @@ -290,12 +361,16 @@ class Scanner extends Component { } handleServiceItemClick = (service) => { - this.setState({ scanner: { ...this.state.scanner, service: service }}); + let scanner = { ...this.state.scanner, service: service }; + + this.setState({ scanner: scanner }, () => { + sessionStorage.setItem('scanner', JSON.stringify(scanner)); + }); } render() { let classes = this.props.classes; - let { loadApi, refreshApi, scanApi, scanner, scan, events, services, history, historyDialog, scanDialog, plusOneDialog } = this.state; + let { loadApi, refreshApi, scanApi, scanner, scan, events, services, historyDialog, scanDialog, plusOneDialog } = this.state; let title = this.getTitle(scanner); @@ -377,7 +452,7 @@ class Scanner extends Component { this.setState({ historyDialog: { open: false }})} /> diff --git a/web/src/scans/ScannerHistoryDialog.js b/web/src/scans/ScannerHistoryDialog.js index 0cef2d66..54c61e72 100644 --- a/web/src/scans/ScannerHistoryDialog.js +++ b/web/src/scans/ScannerHistoryDialog.js @@ -69,9 +69,17 @@ class ScannerHistoryDialog extends Component { let veteranId = scan.response.veteranId; let serviceId = scan.response.serviceId; + let url = '/v1/scans/' + eventId + '/'; + + if (serviceId !== undefined) { + url = url + serviceId + '/'; + } + + url = url + veteranId; + return new Promise((resolve, reject) => { this.setState({ api: { isExecuting: true}}, () => { - this.props.context.api.delete('/v1/scans/' + eventId + '/' + veteranId + (serviceId !== undefined ? '/' + serviceId : '')) + this.props.context.api.delete(url) .then(response => { this.setState({ api: { isExecuting: false, isErrored: false }}, () => { this.props.onDelete(scan); diff --git a/web/src/scans/ScannerMenu.js b/web/src/scans/ScannerMenu.js index dc627b9b..2f9b06fb 100644 --- a/web/src/scans/ScannerMenu.js +++ b/web/src/scans/ScannerMenu.js @@ -122,7 +122,7 @@ class ScannerMenu extends Component { - Reset Event or Service + Reset Scanner @@ -133,7 +133,10 @@ class ScannerMenu extends Component { onConfirm={this.resetScanner} onClose={this.handleConfirmDialogClose} > -

Are you sure you want reset the Scanner?

+
+

Are you sure you want reset the Scanner?

+

This will clear the scan history and prompt you to select the Event and Service again.

+
);