feat(badges): add Cvent Splash XLSX import mode; fix server-side upload timeout
- Add 'Cvent Splash XLSX (registrant export)' upload mode hitting the new
/event/{id}/badge/import/splash_xlsx endpoint
- Admin controls: begin_at/end_at/return_detail (shared with Zoom mode) +
import_status_filter (splash only, default 'Attending')
- File picker accept attribute switches between .csv and .xlsx per mode
- Set timeout=300000 and retry_count=1 on both server-side upload paths to
prevent false 'no response' failures on slow imports; upsert-by-email on
the backend makes retries safe but a single attempt is sufficient
- Replace misleading 0/0 progress bar with an indeterminate progress bar
during server-side processing; real counter kept for client-side CSV mode
- Show 'Processing on server…' message once upload completes and server work begins
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,10 +23,11 @@ let upload_status: string = $state('idle'); // idle, loading, processing, succes
|
||||
let upload_message: string = $state('');
|
||||
let processed_badges_count: number = $state(0);
|
||||
let total_badges_in_file: number = $state(0);
|
||||
let upload_mode: 'client_csv' | 'axonius_zoom' = $state('axonius_zoom');
|
||||
let upload_mode: 'client_csv' | 'axonius_zoom' | 'axonius_splash_xlsx' = $state('axonius_zoom');
|
||||
let begin_at: number | null = $state(null);
|
||||
let end_at: number | null = $state(null);
|
||||
let return_detail: boolean = $state(false);
|
||||
let import_status_filter: string = $state('Attending'); // splash only; set to '' to import all statuses
|
||||
|
||||
// A very basic CSV parser that assumes the first row is headers
|
||||
function parse_csv(text: string): key_val[] {
|
||||
@@ -64,6 +65,72 @@ async function handle_upload(event: Event) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Server-side import for Cvent Splash XLSX
|
||||
if (upload_mode === 'axonius_splash_xlsx') {
|
||||
upload_status = 'loading';
|
||||
upload_message = 'Uploading to server...';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', selected_file, selected_file.name);
|
||||
|
||||
const params: any = {};
|
||||
if (begin_at !== null && begin_at !== undefined) params.begin_at = String(begin_at);
|
||||
if (end_at !== null && end_at !== undefined) params.end_at = String(end_at);
|
||||
if (import_status_filter !== 'Attending') params.import_status_filter = import_status_filter;
|
||||
if (return_detail) params.return_detail = String(true);
|
||||
|
||||
const task_id = (crypto && (crypto as any).randomUUID)
|
||||
? (crypto as any).randomUUID()
|
||||
: String(Date.now());
|
||||
|
||||
try {
|
||||
upload_status = 'processing';
|
||||
upload_message = 'Processing on server… this may take a minute or two.';
|
||||
const endpoint = `/event/${event_id}/badge/import/splash_xlsx`;
|
||||
const result = await api.post_object({
|
||||
api_cfg: $ae_api,
|
||||
endpoint,
|
||||
form_data: formData,
|
||||
params,
|
||||
return_meta: true,
|
||||
task_id,
|
||||
timeout: 300000, // 5 min — server-side import can be slow; no retry to avoid duplicate imports
|
||||
retry_count: 1,
|
||||
log_lvl: 0
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
upload_status = 'error';
|
||||
upload_message = 'Server import failed (no response).';
|
||||
if (onerror) onerror(result);
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = result.meta ?? result;
|
||||
if (meta && meta.success === false) {
|
||||
upload_status = 'error';
|
||||
upload_message = meta?.details?.message || 'Server import failed';
|
||||
if (onerror) onerror(result);
|
||||
return;
|
||||
}
|
||||
|
||||
const processed = meta?.processed ?? meta?.data_list_count ?? (Array.isArray(result?.data) ? result.data.length : 0);
|
||||
processed_badges_count = processed || 0;
|
||||
total_badges_in_file = meta?.total ?? processed_badges_count;
|
||||
|
||||
upload_status = 'success';
|
||||
upload_message = `Server processed ${processed_badges_count} badges.`;
|
||||
if (onsuccess) onsuccess();
|
||||
} catch (err: any) {
|
||||
upload_status = 'error';
|
||||
upload_message = `Upload failed: ${err?.message || 'Unknown error'}`;
|
||||
console.error('Upload error:', err);
|
||||
if (onerror) onerror(err);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Server-side import for Axonius Zoom CSV
|
||||
if (upload_mode === 'axonius_zoom') {
|
||||
upload_status = 'loading';
|
||||
@@ -83,6 +150,7 @@ async function handle_upload(event: Event) {
|
||||
|
||||
try {
|
||||
upload_status = 'processing';
|
||||
upload_message = 'Processing on server… this may take a minute or two.';
|
||||
const endpoint = `/event/${event_id}/badge/import/zoom_csv`;
|
||||
const result = await api.post_object({
|
||||
api_cfg: $ae_api,
|
||||
@@ -91,6 +159,8 @@ async function handle_upload(event: Event) {
|
||||
params,
|
||||
return_meta: true,
|
||||
task_id,
|
||||
timeout: 300000, // 5 min — server-side import can be slow; no retry to avoid duplicate imports
|
||||
retry_count: 1,
|
||||
log_lvl: 0
|
||||
});
|
||||
|
||||
@@ -208,23 +278,27 @@ function handle_cancel() {
|
||||
<code>badge_type_code</code>.
|
||||
</p> -->
|
||||
<p>
|
||||
Upload the standard Axonius Zoom event tickets CSV export file here. This will trigger server-side processing to import the new and changed badges. This can take a few seconds to a few minutes.
|
||||
{#if upload_mode === 'axonius_splash_xlsx'}
|
||||
Upload the Cvent Splash XLSX registrant export file here. Only registrants with status "Attending" are imported by default. Server-side processing can take a few seconds to a few minutes.
|
||||
{:else}
|
||||
Upload the standard Axonius Zoom event tickets CSV export file here. This will trigger server-side processing to import the new and changed badges. This can take a few seconds to a few minutes.
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<fieldset class="space-y-2">
|
||||
<legend class="label"><span>Upload format</span></legend>
|
||||
<div class="flex gap-4 items-center">
|
||||
<!-- <label class="label flex items-center gap-2">
|
||||
<input type="radio" name="upload_mode" value="client_csv" bind:group={upload_mode} />
|
||||
<span>Standard CSV (client-side parse)</span>
|
||||
</label> -->
|
||||
<label class="label flex items-center gap-2">
|
||||
<input type="radio" name="upload_mode" value="axonius_zoom" bind:group={upload_mode} />
|
||||
<span>Axonius Zoom CSV (event tickets)</span>
|
||||
</label>
|
||||
<label class="label flex items-center gap-2">
|
||||
<input type="radio" name="upload_mode" value="axonius_splash_xlsx" bind:group={upload_mode} />
|
||||
<span>Cvent Splash XLSX (registrant export)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if $ae_loc.administrator_access && upload_mode === 'axonius_zoom'}
|
||||
{#if $ae_loc.administrator_access && (upload_mode === 'axonius_zoom' || upload_mode === 'axonius_splash_xlsx')}
|
||||
<div class="grid grid-cols-3 gap-2 items-center">
|
||||
<input type="number" min="0" placeholder="begin_at (0)" bind:value={begin_at} class="input" />
|
||||
<input type="number" min="0" placeholder="end_at (20000)" bind:value={end_at} class="input" />
|
||||
@@ -233,14 +307,25 @@ function handle_cancel() {
|
||||
<span>Return detail</span>
|
||||
</label>
|
||||
</div>
|
||||
{#if upload_mode === 'axonius_splash_xlsx'}
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="label" for="import_status_filter">Status filter</label>
|
||||
<input
|
||||
id="import_status_filter"
|
||||
type="text"
|
||||
placeholder="Attending (leave blank for all)"
|
||||
bind:value={import_status_filter}
|
||||
class="input flex-1" />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</fieldset>
|
||||
|
||||
<label class="label">
|
||||
<span>Select CSV File</span>
|
||||
<span>Select {upload_mode === 'axonius_splash_xlsx' ? 'XLSX' : 'CSV'} File</span>
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv"
|
||||
accept={upload_mode === 'axonius_splash_xlsx' ? '.xlsx' : '.csv'}
|
||||
bind:this={file_input}
|
||||
onchange={handle_file_change}
|
||||
class="input" />
|
||||
@@ -257,13 +342,15 @@ function handle_cancel() {
|
||||
class:preset-tonal-surface={upload_status !== 'error'}>
|
||||
<p>{upload_message}</p>
|
||||
{#if upload_status === 'processing' || upload_status === 'loading'}
|
||||
<progress
|
||||
class="progress"
|
||||
value={processed_badges_count}
|
||||
max={total_badges_in_file}></progress>
|
||||
<p>
|
||||
Processed: {processed_badges_count} / {total_badges_in_file}
|
||||
</p>
|
||||
{#if upload_mode === 'client_csv'}
|
||||
<progress
|
||||
class="progress"
|
||||
value={processed_badges_count}
|
||||
max={total_badges_in_file}></progress>
|
||||
<p>Processed: {processed_badges_count} / {total_badges_in_file}</p>
|
||||
{:else}
|
||||
<progress class="progress"></progress>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user