feat(badges): public_access kiosk mode + manager access improvements
- Public (attendee) kiosk: unprinted badges link to /review; printed badges show green "Checked in · Nx · First/Last" row (non-clickable) - Public attendees no longer see Print button (staff-only action) - Printed badges sort to end of list for public non-trusted users - Manager access: Print (reprint) and Email Link buttons always visible without requiring Edit Mode; main row behavior unchanged - Empty state wording: context-aware — "Enter your name above to find your badge" for public users with no query vs "No badges found" after an actual search - Docs: Epson C3500 fanfold section filled in (was empty placeholder); style_href/duplex implementation status corrected in badge templates doc; Axonius C3500 layout TODO marked complete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -157,26 +157,58 @@ This layout hides `.badge_back` in `@media print` — only the front face prints
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Epson — Fan-Fold / Label Printer
|
### Epson ColorWorks C3500 — Fan-Fold Label Printer
|
||||||
|
|
||||||
**Status:** Not yet tested. Section to be filled in after testing.
|
**Card stock:** 4" × 6" fan-fold paper label stock
|
||||||
|
**Layout code:** `badge_4x6_fanfold`
|
||||||
|
**Status:** Configured. First live use: Axonius Adapt DC — June 9, 2026.
|
||||||
|
|
||||||
Common Epson models used for fan-fold name badge stock: TM-T88 series, C3500, LX series.
|
The C3500 is a color inkjet label printer — it prints continuous fan-fold paper stock,
|
||||||
Fan-fold stock is typically 4" × 3" or 4" × 6" paper labels.
|
not individual cards. Badges are separated along the perforation after printing.
|
||||||
|
|
||||||
|
#### Physical Setup
|
||||||
|
|
||||||
|
- Connect via USB or Ethernet
|
||||||
|
- Load 4" × 6" fan-fold stock per Epson instructions
|
||||||
|
- The C3500 is single-sided — only the front face prints. Back section is suppressed in CSS.
|
||||||
|
- The badge has a lanyard hole punch: 5/8" × 1/8", centered, 1/4" from the top.
|
||||||
|
Most fan-fold stock for badge use includes a pre-punched lanyard slot — verify stock matches.
|
||||||
|
|
||||||
|
#### Driver
|
||||||
|
|
||||||
|
- Epson ColorWorks C3500 CUPS driver available from epson.com (ColorWorks section)
|
||||||
|
- On Linux/CUPS: install the provided PPD and add the printer at `http://localhost:631`
|
||||||
|
- Set default paper size to **4" × 6"** in CUPS
|
||||||
|
- Print a test page from CUPS before going live
|
||||||
|
|
||||||
|
#### Chrome Print Settings (C3500)
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---|---|
|
||||||
|
| Destination | Epson C3500 (CUPS name) |
|
||||||
|
| Paper size | 4 × 6 in (set in CUPS driver) |
|
||||||
|
| Margins | **None** |
|
||||||
|
| Background graphics | On |
|
||||||
|
| Pages | 1 (single-sided) |
|
||||||
|
|
||||||
#### CSS Layout
|
#### CSS Layout
|
||||||
|
|
||||||
Fan-fold badges would use a layout sized to the specific label stock.
|
The C3500 uses the `badge_4x6_fanfold` layout. CSS file:
|
||||||
A new CSS layout file will need to be created per stock size if not already present.
|
`src/lib/ae_events/badges/css/badge_layout_epson_4x6_fanfold.css`
|
||||||
Naming convention: `badge_layout_epson_[model]_[size].css`
|
|
||||||
|
|
||||||
#### Setup Notes
|
Created 2026-05-15 for Axonius Adapt DC. Key specs:
|
||||||
|
- `badge_front` 4" × 6", portrait orientation
|
||||||
*(To be filled in after testing — cover: driver source, CUPS setup, paper size, Chrome settings)*
|
- `badge_header` max-height 1.5in
|
||||||
|
- Lanyard hole: 5/8" × 1/8", centered, 1/4" from top
|
||||||
|
- `@page { size: 4in 6in; margin: 0; }` set in the print page dynamically
|
||||||
|
- `.badge_back` suppressed in `@media print` (single-sided)
|
||||||
|
|
||||||
#### Known Behaviors
|
#### Known Behaviors
|
||||||
|
|
||||||
*(To be filled in after testing)*
|
- Same Chrome margin rules apply: **Margins → None** prevents URL/date header clipping
|
||||||
|
- Firefox honors `@page { size: 4in 6in }` for PDF proofing — use it to verify layout
|
||||||
|
- Fan-fold stock separates along the perforation — no cutting needed, but verify the
|
||||||
|
perforation lands outside the badge content area
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ server needed.
|
|||||||
- **Stock:** 3.5" × 5.5" PVC cards.
|
- **Stock:** 3.5" × 5.5" PVC cards.
|
||||||
- **Orientation:** Cards face-up, landscape in the hopper.
|
- **Orientation:** Cards face-up, landscape in the hopper.
|
||||||
- **Single-Sided:** Only the front face prints; the back section is hidden via CSS.
|
- **Single-Sided:** Only the front face prints; the back section is hidden via CSS.
|
||||||
|
- **Layout code:** `badge_3.5x5.5_pvc`
|
||||||
|
|
||||||
|
### Printer Reference: Epson ColorWorks C3500 (Fan-Fold)
|
||||||
|
- **Stock:** 4" × 6" fan-fold paper label stock.
|
||||||
|
- **Single-Sided:** Only the front face prints; the back section is hidden via CSS.
|
||||||
|
- **Layout code:** `badge_4x6_fanfold`
|
||||||
|
- **Lanyard hole:** Pre-punched 5/8" × 1/8" slot at top center — verify stock matches.
|
||||||
|
- **First live use:** Axonius Adapt DC, June 9, 2026.
|
||||||
|
|
||||||
### Printing Workflow
|
### Printing Workflow
|
||||||
1. **Search:** Find the attendee by name or QR scan in the Badges module.
|
1. **Search:** Find the attendee by name or QR scan in the Badges module.
|
||||||
|
|||||||
@@ -114,17 +114,19 @@ corresponding `ticket_N_text` on the template provides the HTML rendered on the
|
|||||||
| `priority`, `sort`, `group` | int/str | Standard AE sort fields |
|
| `priority`, `sort`, `group` | int/str | Standard AE sort fields |
|
||||||
| `notes` | str | Internal notes |
|
| `notes` | str | Internal notes |
|
||||||
|
|
||||||
### New Field (pending backend addition)
|
### Duplex / Single-Sided
|
||||||
| Field | Type | Notes |
|
| Field | Type | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `duplex` | bool | **Planned** — when `false`, back section is hidden from print (`@media print`) |
|
| `duplex` | bool | When `false`, back section is hidden from print (`@media print`) |
|
||||||
|
|
||||||
The `duplex` field controls whether the back-of-badge section renders during printing.
|
The `duplex` field controls whether the back-of-badge section renders during printing.
|
||||||
When `false` (single-sided), `badge_back` gets `print:hidden` applied so only the front
|
When `false` (single-sided), `badge_back` gets `print:hidden` applied so only the front
|
||||||
prints. The back section still displays on screen for configuration reference.
|
prints. The back section still displays on screen for configuration reference.
|
||||||
|
|
||||||
The first event using this system (Axonius, NYC, mid-April 2026) uses single-sided PVC
|
`duplex` is in `properties_to_save` and `show_badge_back` is derived from it in
|
||||||
cards on a Zebra ZC10L — `duplex` will be `false` for that event's templates.
|
`ae_comp__badge_obj_view.svelte`. (Verified 2026-03-18)
|
||||||
|
|
||||||
|
Axonius events use `duplex = false` — single-sided printing only.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -155,7 +157,7 @@ The print page (`print/+page.svelte`) or the badge view should conditionally add
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
```
|
```
|
||||||
|
|
||||||
This is not yet implemented — tracked as a pending Phase 1 item.
|
This is implemented — `style_href` loads via `<svelte:head>` in `print/+page.svelte` and is included in `properties_to_save`. (Verified 2026-03-18)
|
||||||
|
|
||||||
### CSS Scope
|
### CSS Scope
|
||||||
|
|
||||||
@@ -218,7 +220,6 @@ The `properties_to_save` array in `ae_events__event_badge_template.ts` controls
|
|||||||
gets cached locally. Current state — fields **NOT** in properties_to_save that exist
|
gets cached locally. Current state — fields **NOT** in properties_to_save that exist
|
||||||
in DB and may be needed:
|
in DB and may be needed:
|
||||||
|
|
||||||
- `style_href` — needed once external CSS is wired via `<svelte:head>`
|
|
||||||
- `passcode` — not needed client-side
|
- `passcode` — not needed client-side
|
||||||
- `footer_title`, `footer_left`, `footer_right` — not needed (legacy)
|
- `footer_title`, `footer_left`, `footer_right` — not needed (legacy)
|
||||||
- `header_background`, `footer_background` — not needed (legacy)
|
- `header_background`, `footer_background` — not needed (legacy)
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
## 🔴 Axonius DC — June 9 (Badge Printing)
|
## 🔴 Axonius DC — June 9 (Badge Printing)
|
||||||
**Setup/Registration:** June 8 | **Show:** June 9
|
**Setup/Registration:** June 8 | **Show:** June 9
|
||||||
|
|
||||||
- [ ] **[Badges] Epson C3500 fanfold badge layout** — Create/configure a fanfold badge layout
|
- [x] **[Badges] Epson C3500 fanfold badge layout** — `badge_4x6_fanfold` layout CSS created,
|
||||||
compatible with the Epson C3500 continuous stock format.
|
wired, and documented. First live use: Axonius Adapt DC, June 9, 2026. (2026-05-15)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ let copy_status: Record<string, 'idle' | 'copied'> = $state({});
|
|||||||
// Access level shortcuts
|
// Access level shortcuts
|
||||||
let is_trusted = $derived($ae_loc.trusted_access === true);
|
let is_trusted = $derived($ae_loc.trusted_access === true);
|
||||||
let is_admin = $derived($ae_loc.administrator_access === true);
|
let is_admin = $derived($ae_loc.administrator_access === true);
|
||||||
|
let is_manager = $derived($ae_loc.manager_access === true);
|
||||||
let is_public = $derived($ae_loc.public_access === true); // public passcode or higher — may print first prints
|
let is_public = $derived($ae_loc.public_access === true); // public passcode or higher — may print first prints
|
||||||
let is_edit_mode = $derived($ae_loc.edit_mode === true);
|
let is_edit_mode = $derived($ae_loc.edit_mode === true);
|
||||||
|
|
||||||
@@ -113,12 +114,24 @@ let visible_badge_obj_li = $derived(
|
|||||||
if (ps === 'not_printed') return (item.print_count ?? 0) < 1 && hide_ok;
|
if (ps === 'not_printed') return (item.print_count ?? 0) < 1 && hide_ok;
|
||||||
return hide_ok; // 'all'
|
return hide_ok; // 'all'
|
||||||
}
|
}
|
||||||
// Public (kiosk) / authenticated / anonymous: only unprinted, never hidden.
|
// Public/Attendee (public_access, not trusted): show all non-hidden.
|
||||||
// Badge kiosks run at public_access — attendees should only see their own
|
// Printed badges appear read-only so attendees can see their check-in status.
|
||||||
// unprinted badge, never a list of already-printed ones.
|
if (is_public) return !item.hide;
|
||||||
|
// Below public (anonymous, authenticated): only unprinted, never hidden.
|
||||||
return (item.print_count ?? 0) < 1 && !item.hide;
|
return (item.print_count ?? 0) < 1 && !item.hide;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// For public (attendee) kiosk: sort unprinted first so attendees find their
|
||||||
|
// own badge at the top; already-checked-in records fall to the bottom.
|
||||||
|
if (is_public && !is_trusted && filtered.length > 1) {
|
||||||
|
filtered.sort((a: any, b: any) => {
|
||||||
|
const a_p = (a.print_count ?? 0) > 0;
|
||||||
|
const b_p = (b.print_count ?? 0) > 0;
|
||||||
|
if (a_p !== b_p) return a_p ? 1 : -1;
|
||||||
|
return 0; // preserve existing name order within each group
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (log_lvl)
|
if (log_lvl)
|
||||||
console.log(
|
console.log(
|
||||||
`visible_badge_obj_li: Input=${list.length}, Output=${filtered.length}`
|
`visible_badge_obj_li: Input=${list.length}, Output=${filtered.length}`
|
||||||
@@ -154,9 +167,13 @@ let visible_badge_obj_li = $derived(
|
|||||||
event_badge_obj?.full_name_override ??
|
event_badge_obj?.full_name_override ??
|
||||||
event_badge_obj?.full_name ??
|
event_badge_obj?.full_name ??
|
||||||
`${event_badge_obj?.given_name ?? ''} ${event_badge_obj?.family_name ?? ''}`.trim()}
|
`${event_badge_obj?.given_name ?? ''} ${event_badge_obj?.family_name ?? ''}`.trim()}
|
||||||
{@const can_print = (is_public && !is_printed) || (is_trusted && is_edit_mode)}
|
{@const is_attendee_view = is_public && !is_trusted}
|
||||||
|
{@const is_attendee_printed = is_attendee_view && is_printed}
|
||||||
|
{@const can_print = is_trusted && (!is_printed || is_edit_mode)}
|
||||||
|
{@const row_clickable = can_print || (is_attendee_view && !is_printed)}
|
||||||
{@const print_href = `/events/${event_badge_obj?.event_id}/badges/${event_badge_obj?.event_badge_id}/print`}
|
{@const print_href = `/events/${event_badge_obj?.event_id}/badges/${event_badge_obj?.event_badge_id}/print`}
|
||||||
{@const review_href = build_review_url(event_badge_obj)}
|
{@const review_href = build_review_url(event_badge_obj)}
|
||||||
|
{@const row_href = is_trusted ? print_href : review_href}
|
||||||
|
|
||||||
<li
|
<li
|
||||||
class="
|
class="
|
||||||
@@ -169,18 +186,13 @@ let visible_badge_obj_li = $derived(
|
|||||||
class="flex w-full flex-row flex-wrap items-center justify-between gap-2">
|
class="flex w-full flex-row flex-wrap items-center justify-between gap-2">
|
||||||
<!-- Left cluster: main action (name + email + affiliations) -->
|
<!-- Left cluster: main action (name + email + affiliations) -->
|
||||||
<div class="flex min-w-0 grow flex-row flex-wrap items-center gap-x-3 gap-y-1">
|
<div class="flex min-w-0 grow flex-row flex-wrap items-center gap-x-3 gap-y-1">
|
||||||
{#if can_print}
|
{#if row_clickable}
|
||||||
<a
|
<a
|
||||||
href={print_href}
|
href={row_href}
|
||||||
class="hover:text-primary-800-200 hover:bg-primary-200-800 active:bg-surface-200-700 flex items-center gap-3 justify-start px-3 py-2 text-left text-lg font-bold flex-1 transition-colors duration-1000 hover:duration-300 min-w-0 preset-tonal-primary rounded-lg"
|
class="hover:text-primary-800-200 hover:bg-primary-200-800 active:bg-surface-200-700 flex items-center gap-3 justify-start px-3 py-2 text-left text-lg font-bold flex-1 transition-colors duration-1000 hover:duration-300 min-w-0 preset-tonal-primary rounded-lg"
|
||||||
title="
|
title={is_trusted
|
||||||
Print badge for
|
? `Print badge for ${display_name} · ${event_badge_obj?.affiliations_override ?? event_badge_obj?.affiliations ?? ''} · ${event_badge_obj?.badge_type ?? ''} · id: ${event_badge_obj.event_badge_id}`
|
||||||
{display_name}
|
: `Review your badge info`}
|
||||||
email: <{is_trusted ? event_badge_obj?.email : obscure_email(event_badge_obj?.email)}>
|
|
||||||
{event_badge_obj?.affiliations_override ?? event_badge_obj?.affiliations}
|
|
||||||
type: {event_badge_obj?.badge_type}
|
|
||||||
id: {event_badge_obj.event_badge_id}
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
{#if event_badge_obj?.hide}
|
{#if event_badge_obj?.hide}
|
||||||
<EyeOff size="1.1em" class="text-gray-400" />
|
<EyeOff size="1.1em" class="text-gray-400" />
|
||||||
@@ -208,6 +220,23 @@ let visible_badge_obj_li = $derived(
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
{:else if is_attendee_printed}
|
||||||
|
<!-- Public attendee, badge already printed: show check-in status, no action -->
|
||||||
|
<div class="flex items-center gap-3 justify-start px-3 py-2 flex-1 min-w-0 rounded-lg bg-success-500/10">
|
||||||
|
<Check size="1.1em" class="text-success-600 dark:text-success-400 shrink-0" />
|
||||||
|
<div class="flex flex-col min-w-0">
|
||||||
|
<span class="truncate font-semibold">{display_name}</span>
|
||||||
|
<span class="text-xs text-success-700 dark:text-success-300 opacity-80">
|
||||||
|
Checked in · {print_count}×
|
||||||
|
{#if event_badge_obj.print_first_datetime}
|
||||||
|
· First: {ae_util.iso_datetime_formatter(event_badge_obj.print_first_datetime, 'datetime_iso_12_no_seconds')}
|
||||||
|
{/if}
|
||||||
|
{#if event_badge_obj.print_last_datetime}
|
||||||
|
· Last: {ae_util.iso_datetime_formatter(event_badge_obj.print_last_datetime, 'datetime_iso_12_no_seconds')}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="hover:text-primary-800-200 hover:bg-primary-200-800 active:bg-surface-200-700 flex items-center gap-3 justify-start px-3 py-2 text-left text-lg font-bold flex-1 transition-colors duration-1000 hover:duration-300 min-w-0 preset-tonal-primary rounded-lg cursor-not-allowed"
|
class="hover:text-primary-800-200 hover:bg-primary-200-800 active:bg-surface-200-700 flex items-center gap-3 justify-start px-3 py-2 text-left text-lg font-bold flex-1 transition-colors duration-1000 hover:duration-300 min-w-0 preset-tonal-primary rounded-lg cursor-not-allowed"
|
||||||
@@ -256,8 +285,8 @@ let visible_badge_obj_li = $derived(
|
|||||||
|
|
||||||
<!-- Right: up to 4 action buttons -->
|
<!-- Right: up to 4 action buttons -->
|
||||||
<div class="flex shrink-0 flex-row items-center gap-1">
|
<div class="flex shrink-0 flex-row items-center gap-1">
|
||||||
<!-- 1. Print Badge: Public+ first print; Trusted + Edit Mode for reprint -->
|
<!-- 1. Print Badge: Trusted — first print anytime, reprint in Edit Mode or Manager+ -->
|
||||||
{#if (is_public && !is_printed) || (is_trusted && is_edit_mode)}
|
{#if is_trusted && (!is_printed || is_edit_mode || is_manager)}
|
||||||
<a
|
<a
|
||||||
href={`/events/${event_badge_obj?.event_id}/badges/${event_badge_obj?.event_badge_id}/print`}
|
href={`/events/${event_badge_obj?.event_id}/badges/${event_badge_obj?.event_badge_id}/print`}
|
||||||
class="hover:text-primary-800-200 hover:bg-primary-200-800 active:bg-surface-200-700 flex items-center gap-1 px-3 py-2 text-base font-bold transition-colors duration-1000 hover:duration-300 min-w-0 preset-tonal-primary rounded-lg border border-primary-200-800
|
class="hover:text-primary-800-200 hover:bg-primary-200-800 active:bg-surface-200-700 flex items-center gap-1 px-3 py-2 text-base font-bold transition-colors duration-1000 hover:duration-300 min-w-0 preset-tonal-primary rounded-lg border border-primary-200-800
|
||||||
@@ -312,9 +341,8 @@ let visible_badge_obj_li = $derived(
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- 4. Email Review Link: Administrator + Edit Mode only
|
<!-- 4. Email Review Link: Manager+ always; Administrator + Edit Mode otherwise -->
|
||||||
Temporarily restricted — TODO: restore broader access after Axonius 2026 -->
|
{#if is_admin && (is_edit_mode || is_manager)}
|
||||||
{#if is_admin && is_edit_mode}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="hover:text-primary-800-200 hover:bg-primary-200-800 active:bg-surface-200-700 flex items-center gap-1 px-3 py-2 text-base font-bold transition-colors duration-1000 hover:duration-300 min-w-0 preset-tonal-primary rounded-lg border border-primary-200-800"
|
class="hover:text-primary-800-200 hover:bg-primary-200-800 active:bg-surface-200-700 flex items-center gap-1 px-3 py-2 text-base font-bold transition-colors duration-1000 hover:duration-300 min-w-0 preset-tonal-primary rounded-lg border border-primary-200-800"
|
||||||
@@ -382,10 +410,11 @@ let visible_badge_obj_li = $derived(
|
|||||||
<div
|
<div
|
||||||
class="flex flex-col items-center justify-center p-20 text-center opacity-50">
|
class="flex flex-col items-center justify-center p-20 text-center opacity-50">
|
||||||
<FileSearch size="3em" class="mx-auto mb-2 opacity-20" />
|
<FileSearch size="3em" class="mx-auto mb-2 opacity-20" />
|
||||||
<p>
|
{#if !is_trusted && !(badges_loc.current.fulltext_search_qry_str ?? '').trim()}
|
||||||
No badges found matching your criteria. Try adjusting your
|
<p>Enter your name above to find your badge.</p>
|
||||||
filters.
|
{:else}
|
||||||
</p>
|
<p>No badges found matching your criteria. Try adjusting your filters.</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user