Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions src/app/pages/map/map.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,33 @@
<ion-menu-button [color]="liveUpdateService.needsUpdate ? 'primary' : 'medium'"></ion-menu-button>
<ion-badge *ngIf="liveUpdateService.needsUpdate" size=sm>1</ion-badge>
</ion-buttons>
<ion-title>Lead Retrieval</ion-title>
<ion-title>Lead Scanner</ion-title>
</ion-toolbar>
</ion-header>

<ion-content [style.visibility]="content_visibility">
<ion-refresher slot="fixed" (ionRefresh)="handleRefresh($event)">
<ion-refresher-content></ion-refresher-content>
</ion-refresher>

<ion-item *ngIf="isStaffScanner && selectedSponsor" color="primary" button="true" (click)="showSponsorSelector()">
<ion-icon slot="start" name="business-outline"></ion-icon>
<ion-label>
<h2>Scanning for: {{ selectedSponsor.name }}</h2>
<p>Tap to change sponsor</p>
</ion-label>
<ion-icon slot="end" name="swap-horizontal-outline"></ion-icon>
</ion-item>

<ion-item *ngIf="isStaffScanner && !selectedSponsor" color="warning">
<ion-icon slot="start" name="alert-circle-outline"></ion-icon>
<ion-label class="ion-text-wrap">
<h2>No sponsor selected</h2>
<p>Select a sponsor before scanning</p>
</ion-label>
<ion-button slot="end" fill="clear" (click)="showSponsorSelector()">Select</ion-button>
</ion-item>

<ion-list>
<ion-list-header>
<ion-label>Recent Scans</ion-label>
Expand Down Expand Up @@ -102,7 +121,7 @@ <h1>
</ion-card>
<ion-toolbar class="footer-toolbar">
<ion-buttons class="footer-buttons">
<ion-button fill="solid" color="success" class="footer-button" (click)="startScan()" [style.visibility]="scan_start_button_visibility">
<ion-button fill="solid" color="success" class="footer-button" (click)="startScan()" [style.visibility]="scan_start_button_visibility" [disabled]="isStaffScanner && !selectedSponsor">
Start Scanner!
</ion-button>
<ion-button fill="solid" color="danger" class="footer-button" (click)="stopScan()" [style.visibility]="scan_stop_button_visibility">
Expand Down
69 changes: 67 additions & 2 deletions src/app/pages/map/map.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Component, ElementRef, ChangeDetectorRef, Inject, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { ConferenceData } from '../../providers/conference-data';
import { Config, Platform } from '@ionic/angular';
import { AlertController, Config, Platform } from '@ionic/angular';
import { StartScanOptions, BarcodeScanner, BarcodeFormat, LensFacing, ScanResult } from '@capacitor-mlkit/barcode-scanning';
import { Storage } from '@ionic/storage-angular';
import { ModalController } from '@ionic/angular';

import { PyConAPI } from '../../providers/pycon-api';
import { UserData } from '../../providers/user-data';
import { LiveUpdateService } from '../../providers/live-update.service';
import { LeadNoteModalComponent } from '../../lead-note-modal/lead-note-modal.component';

Expand All @@ -28,13 +29,18 @@ export class MapPage implements OnInit, OnDestroy {

ios: boolean;
show_permissions_error: boolean = false;
isStaffScanner: boolean = false;
selectedSponsor: any = null;
sponsorList: any[] = [];

constructor(
public confData: ConferenceData,
public platform: Platform,
private config: Config,
private pycon: PyConAPI,
private userData: UserData,
private storage: Storage,
private alertCtrl: AlertController,
public modalCtrl: ModalController,
public liveUpdateService: LiveUpdateService,
public detectorRef: ChangeDetectorRef,
Expand Down Expand Up @@ -231,11 +237,70 @@ export class MapPage implements OnInit, OnDestroy {
this.stopScan();
}

ngOnInit(): void {
async ngOnInit() {
BarcodeScanner.removeAllListeners();
this.ios = this.config.get('mode') === `ios`;
this.refresh_presentation();
setTimeout(this.syncAllPending, 60000);

// Check if this is a staff user (has door_check but not lead_retrieval)
const hasLeadRetrieval = await this.userData.checkHasLeadRetrieval();
const hasDoorCheck = await this.userData.checkHasDoorCheck();
if (!hasLeadRetrieval && hasDoorCheck) {
this.isStaffScanner = true;
this.showSponsorSelector();
}
}

async showSponsorSelector() {
try {
const result: any = await this.pycon.fetchLeadRetrievalSponsors();
this.sponsorList = result.sponsors || [];
} catch (e) {
console.log('Failed to fetch sponsors for staff scanner', e);
const errorAlert = await this.alertCtrl.create({
header: 'Unable to load sponsors',
message: 'Could not fetch the sponsor list. Check your network connection or try again later.',
buttons: ['OK'],
});
await errorAlert.present();
return;
}

if (this.sponsorList.length === 0) {
const emptyAlert = await this.alertCtrl.create({
header: 'No sponsors available',
message: 'No sponsors with lead retrieval are configured yet.',
buttons: ['OK'],
});
await emptyAlert.present();
return;
}

const inputs = this.sponsorList.map(s => ({
type: 'radio' as const,
label: s.name + (s.booth_number ? ` (Booth ${s.booth_number})` : ''),
value: String(s.id),
}));

const alert = await this.alertCtrl.create({
header: 'Scan on behalf of',
message: 'Select which sponsor you are scanning for:',
inputs,
backdropDismiss: false,
buttons: [
{
text: 'Select',
handler: (sponsorId) => {
if (sponsorId) {
this.selectedSponsor = this.sponsorList.find(s => String(s.id) === sponsorId);
this.pycon.setStaffSponsorId(sponsorId);
}
}
}
]
});
await alert.present();
}

ngOnDestroy(): void {
Expand Down
25 changes: 25 additions & 0 deletions src/app/pages/tabs-page/tabs-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,29 @@

</ion-tab-bar>

<ion-modal #staffToolsModal [isOpen]="showStaffTools" (didDismiss)="showStaffTools = false"
[breakpoints]="[0, 0.35]" [initialBreakpoint]="0.35" [handle]="true">
<ng-template>
<ion-content class="staff-tools-sheet">
<div class="staff-tools-header">
<h2>Staff Tools</h2>
</div>
<ion-list lines="full">
<ion-item *ngIf="hasDoorCheck" button="true" (click)="goToTool('/app/tabs/door-check')" detail="true">
<ion-icon slot="start" name="checkbox-outline" color="primary"></ion-icon>
<ion-label>Door Check</ion-label>
</ion-item>
<ion-item *ngIf="hasDoorCheck" button="true" (click)="goToTool('/app/tabs/t-shirt-redemption')" detail="true">
<ion-icon slot="start" name="shirt-outline" color="primary"></ion-icon>
<ion-label>Swag Pickup</ion-label>
</ion-item>
<ion-item *ngIf="hasLeadRetrieval || hasDoorCheck" button="true" (click)="goToTool('/app/tabs/lead-retrieval')" detail="true">
<ion-icon slot="start" name="qr-code-outline" color="primary"></ion-icon>
<ion-label>Lead Scanner</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ng-template>
</ion-modal>

</ion-tabs>
15 changes: 15 additions & 0 deletions src/app/pages/tabs-page/tabs-page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,18 @@
padding: .25em;
}

.staff-tools-sheet {
--background: var(--ion-background-color, #fff);
}

.staff-tools-header {
padding: 16px 20px 8px;

h2 {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
color: var(--ion-text-color);
}
}

39 changes: 9 additions & 30 deletions src/app/pages/tabs-page/tabs-page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { ActionSheetController } from '@ionic/angular';
import { ModalController } from '@ionic/angular';
import { Router } from '@angular/router';

import { ConferenceData } from '../../providers/conference-data';
Expand All @@ -15,11 +15,12 @@ export class TabsPage implements OnInit {
hasLeadRetrieval: boolean = false;
hasDoorCheck: boolean = false;
loggedIn: boolean = false;
showStaffTools: boolean = false;

constructor(
private userData: UserData,
private confData: ConferenceData,
private actionSheetCtrl: ActionSheetController,
private modalCtrl: ModalController,
private router: Router,
) {}

Expand All @@ -38,37 +39,15 @@ export class TabsPage implements OnInit {
});
}

async openStaffTools(event: Event) {
openStaffTools(event: Event) {
event.stopPropagation();
event.preventDefault();
this.showStaffTools = true;
}

const buttons = [];
if (this.hasDoorCheck) {
buttons.push({
text: 'Door Check',
icon: 'checkbox-outline',
handler: () => { this.router.navigateByUrl('/app/tabs/door-check'); }
});
buttons.push({
text: 'Swag Pickup',
icon: 'gift-outline',
handler: () => { this.router.navigateByUrl('/app/tabs/t-shirt-redemption'); }
});
}
if (this.hasLeadRetrieval) {
buttons.push({
text: 'Lead Retrieval',
icon: 'qr-code-outline',
handler: () => { this.router.navigateByUrl('/app/tabs/lead-retrieval'); }
});
}
buttons.push({ text: 'Cancel', role: 'cancel', icon: 'close' });

const actionSheet = await this.actionSheetCtrl.create({
header: 'Staff Tools',
buttons
});
await actionSheet.present();
goToTool(path: string) {
this.showStaffTools = false;
this.router.navigateByUrl(path);
}

showSponsorBanner() {
Expand Down
29 changes: 27 additions & 2 deletions src/app/providers/pycon-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,25 @@ export class PyConAPI {
)
}

async fetchLeadRetrievalSponsors(): Promise<any> {
const method = 'GET';
const url = '/2026/api/v1/lead_retrieval/sponsors/';
const body = '';
const authHeaders = await this.buildRequestAuthHeaders(method, url, body);
return this.http.get(
this.base + url,
{headers: authHeaders}
).pipe(timeout(5000)).toPromise();
}

async setStaffSponsorId(sponsorId: string) {
await this.storage.set('staff-sponsor-id', sponsorId);
}

async clearStaffSponsorId() {
await this.storage.remove('staff-sponsor-id');
}

async syncScan(accessCode: string): Promise<any> {
const pending = await this.storage.get('pending-scan-' + accessCode).then((value) => {
return value
Expand All @@ -239,7 +258,11 @@ export class PyConAPI {
const _validator = scanData[scanData.length - 1];

const method = 'GET';
const url = '/2026/api/v1/lead_retrieval/capture/?' + 'attendee_access_code=' + encodeURIComponent(accessCode) + "&badge_validator=" + encodeURIComponent(_validator);
const sponsorId = await this.storage.get('staff-sponsor-id');
let url = '/2026/api/v1/lead_retrieval/capture/?' + 'attendee_access_code=' + encodeURIComponent(accessCode) + "&badge_validator=" + encodeURIComponent(_validator);
if (sponsorId) {
url += '&sponsor_id=' + encodeURIComponent(sponsorId);
}
const body = '';

const authHeaders = await this.buildRequestAuthHeaders(method, url, body);
Expand Down Expand Up @@ -284,9 +307,11 @@ export class PyConAPI {
console.log('Unable to sync note for missing ' + accessCode);
}

const sponsorId = await this.storage.get('staff-sponsor-id');
const noteData = sponsorId ? {...pending, sponsor_id: sponsorId} : pending;
const method = 'POST';
const url = '/2026/api/v1/lead_retrieval/' + accessCode + "/note/";
const body = JSON.stringify(pending);
const body = JSON.stringify(noteData);
const headers = {"Content-Type": "application/json"}

const authHeaders = await this.buildRequestAuthHeaders(method, url, body);
Expand Down
Loading