Features

Sales (POS)

Event-based Point of Sale system for markets and pop-up events.

Sales (POS)

Event-based Point of Sale system for markets and pop-up events.

Status: Beta -- under active development. API may change.

Overview

The Sales package (@fyit/crouton-sales) provides a complete Point of Sale system designed for pop-up events, markets, and temporary retail situations. It includes customer-facing order interfaces, cart management, helper (volunteer/staff) authentication, and optional thermal receipt printing.

Features:

  • Event-based product and category management
  • Shopping cart with quantity controls and product options
  • PIN-based helper/volunteer authentication (via @fyit/crouton-auth)
  • Client selector with create-on-type
  • Offline awareness banner
  • Optional ESC/POS thermal receipt printing
  • i18n support (English, Dutch, French)

Quick Start

1. Install and Configure

Add the sales layer to your nuxt.config.ts:

export default defineNuxtConfig({
  extends: [
    '@fyit/crouton',
    '@fyit/crouton-auth',   // Required for helper authentication
    '@fyit/crouton-sales',
    './layers/sales'        // Generated layer (see step 3)
  ]
})

2. Copy Schemas

Copy the JSON schema files from the package to your project's schemas/ directory:

cp node_modules/@fyit/crouton-sales/schemas/*.json ./schemas/

3. Generate Collections

Configure crouton.config.js with sales as the layer name, then generate:

pnpm crouton config

This generates the ./layers/sales/ layer with collection composables like useSalesProducts(), useSalesCategories(), etc.

4. Run Migrations

npx nuxt db:generate
npx nuxt db:migrate

5. Use the Order Interface

<script setup lang="ts">
const eventId = 'your-event-id'
</script>

<template>
  <SalesClientOrderInterface :event-id="eventId" />
</template>

Composables

usePosOrder(options?)

Cart management, price calculation, and checkout. This composable manages the full cart lifecycle.

const {
  // State (reactive)
  cartItems,           // Readonly<Ref<CartItem[]>>
  selectedEventId,     // Ref<string | null>
  selectedClientId,    // Ref<string | null>
  selectedClientName,  // Ref<string | null>
  overallRemarks,      // Ref<string | null>
  isPersonnel,         // Ref<boolean>
  isCheckingOut,       // Readonly<Ref<boolean>>

  // Computed
  cartTotal,           // ComputedRef<number>
  cartItemCount,       // ComputedRef<number>
  isEmpty,             // ComputedRef<boolean>

  // Methods
  addToCart,            // (product, remarks?, selectedOptions?) => void
  removeFromCart,       // (index: number) => void
  updateQuantity,      // (index: number, quantity: number) => void
  clearCart,            // () => void
  getItemPrice,        // (item: CartItem) => number
  getItemTotal,        // (item: CartItem) => number
  checkout,            // () => Promise<CreateOrderResponse>
} = usePosOrder(options?)

Options

interface UsePosOrderOptions {
  /** API base path for orders, defaults to '/api/sales/events' */
  apiBasePath?: string
  /** Whether to trigger print queue after checkout */
  enablePrinting?: boolean
}

Usage

const { cartItems, addToCart, selectedEventId, checkout } = usePosOrder()

// Set the event context
selectedEventId.value = 'event-123'

// Add a product (increments quantity if already in cart)
addToCart(product)

// Add with remarks and options (always added as new line item)
addToCart(product, 'Extra sauce', ['option-1', 'option-2'])

// Checkout -- creates order via POST to apiBasePath/{eventId}/orders
const response = await checkout()
// response: { order: { id, eventOrderNumber, status }, items, eventOrderNumber }

calculateItemPrice(item)

Utility function defined inside usePosOrder.ts that calculates an item's price including option price modifiers. It is exported alongside the composable and auto-imported when the @fyit/crouton-sales layer is extended -- no import statement is needed:

// Auto-imported in Nuxt layer context -- no import needed
const price = calculateItemPrice(cartItem)

useHelperAuth()

PIN-based authentication for event helpers (volunteers, staff). Wraps @fyit/crouton-auth's scoped access system.

const {
  // State (reactive)
  isHelper,            // ComputedRef<boolean>
  helperName,          // ComputedRef<string>
  eventId,             // ComputedRef<string>
  teamId,              // ComputedRef<string>
  token,               // ComputedRef<string>
  helperSession,       // Readonly<Ref<HelperSession | null>>
  isLoading,           // Readonly<Ref<boolean>>
  error,               // Readonly<Ref<string | null>>

  // Methods
  login,               // (options: HelperLoginOptions) => Promise<boolean>
  logout,              // () => Promise<void>
  validateToken,       // () => Promise<boolean>
  loadSession,         // () => HelperSession | null
  setSession,          // (session: HelperSession) => void
  clearSession,        // () => void
} = useHelperAuth()

Login Options

interface HelperLoginOptions {
  teamId: string
  eventId: string
  pin: string
  helperName?: string   // For new helpers
  helperId?: string     // For returning helpers
}

Usage

const { isHelper, helperName, login, logout } = useHelperAuth()

// Login with PIN
const success = await login({
  teamId: 'team-123',
  eventId: 'event-456',
  pin: '1234',
  helperName: 'John'
})

if (isHelper.value) {
  console.log(`Welcome, ${helperName.value}!`)
}

await logout()

Sessions are stored in localStorage and a cookie (pos-helper-token) with an 8-hour expiry. The composable automatically loads existing sessions on client-side mount.

Components

All components are auto-imported with the Sales prefix.

Customer-Facing (Client/)

ComponentPropsDescription
SalesClientOrderInterfaceeventId (required), productsCollection?, categoriesCollection?Main order page -- combines category tabs, product grid, cart sidebar, options modal, and mobile drawer
SalesClientProductListproductsProduct grid with inline option selection (single/multi-select)
SalesClientCategoryTabscategories, modelValue, productCountsCategory tab navigation with product counts
SalesClientCartitems, total, disabled, clientRequired?, hasClient?Shopping cart with quantity controls. Emits: updateQuantity, remove, checkout, clear
SalesClientCartTotalcount, total, size? ('sm' or 'lg')Order total display with animated item count badge
SalesClientProductOptionsSelectmodelValue, options, multipleAllowed?Product option picker (grid of selectable cards)
SalesClientSelectorclients, useReusableClients, highlight?, clientId?, clientName?, collectionName?Client selector -- either a searchable dropdown with create-on-type or a free-text input
SalesClientOfflineBanner(none)Shows a warning banner when the browser is offline

Orders (Pos/)

ComponentPropsDescription
SalesPosOrdersListeventId?, collectionName?, showReprint?, printApiBasePath?, refreshInterval?Orders table with status filter tabs, auto-refresh toggle, and optional reprint button

Admin (Admin/)

ComponentPropsDescription
SalesAdminPosSidebarbasePath (required), showPrinters?, showHelpers?, additionalItems?Vertical navigation menu for POS admin (events, products, categories, locations, clients)
ComponentPropsDescription
SalesSettingsReceiptSettingsModalmodelValue, apiEndpointModal for customizing receipt text (headers, footer, section titles)
SalesSettingsPrintPreviewModalmodelValue, printer, testPrintApiBase, receiptSettings, locationName?Receipt preview modal with visual preview and test print button

Schemas (Collections)

The package provides JSON schemas for 10 collections. All use the sales prefix when generated.

Core Collections

SchemaTable NameKey Fields
events.jsonsalesEventstitle, slug, startDate, endDate, status, isCurrent, helperPin
products.jsonsalesProductseventId, categoryId, locationId, title, price, isActive, hasOptions, options (repeater)
categories.jsonsalesCategorieseventId, title, displayOrder
orders.jsonsalesOrderseventId, clientId, clientName, eventOrderNumber, status, isPersonnel, overallRemarks
orderItems.jsonsalesOrderitemsorderId, productId, quantity, unitPrice, totalPrice, remarks, selectedOptions
locations.jsonsalesLocationseventId, title
clients.jsonsalesClientstitle, isReusable
eventSettings.jsonsalesEventsettingseventId, settingKey, settingValue
SchemaTable NameKey Fields
printers.jsonsalesPrinterseventId, locationId, title, ipAddress, port, showPrices, isActive
printQueues.jsonsalesPrintqueueseventId, orderId, printerId, status (0=pending, 1=printing, 2=done, 9=error), printData, printMode

Server Utilities

The package provides server-side utilities for thermal receipt printing. These are imported from the package but require your generated layer's database tables to function.

Receipt Formatter

Note: These server utilities are auto-imported by Nuxt when you extend the @fyit/crouton-sales layer. The import paths shown below are for reference only -- in practice, you do not need explicit import statements in your Nuxt server routes.
// Auto-imported in Nuxt server context -- no import needed
// Internal path (for reference): @fyit/crouton-sales/server/utils/receipt-formatter

// Format a receipt -- returns { base64: string, rawBuffer: Buffer }
const receipt = formatReceipt({
  orderNumber: 42,
  orderId: 'order-123',
  teamName: 'My Team',
  eventName: 'Summer Market',
  items: [{ name: 'Coffee', quantity: 2, price: 3.50 }],
  total: 7.00,
  printMode: 'receipt',  // 'kitchen' or 'receipt'
  showPrices: true,
  createdAt: new Date()
})

// Generate a test receipt
const testReceipt = formatTestReceipt('Kitchen Printer', '192.168.1.100')
// Auto-imported in Nuxt server context -- no import needed
// Internal path (for reference): @fyit/crouton-sales/server/utils/print-queue-service

// Generate print jobs (returns data only -- you insert into your DB)
const jobs = generatePrintJobsForOrder(orderOptions, orderItems, printers)

for (const job of jobs) {
  await db.insert(salesPrintqueues).values({
    teamId, eventId, orderId,
    printerId: job.printerId,
    printData: job.printData,
    printMode: job.printMode,
    status: PRINT_STATUS.PENDING,
    retryCount: 0
  })
}

API Routes

This package does not ship API routes. API routes are generated per-app via the crouton CLI when you run pnpm crouton config. The generated ./layers/sales/ layer provides standard CRUD endpoints for each collection.

For the print server (polling jobs, updating status), see the endpoint templates in @fyit/crouton-sales/server/api/sales/print-server/README.md. These templates must be copied and adapted to your project.

Helper Authentication Flow

  1. Admin creates an event with a helperPin field (up to 6 characters)
  2. Helper opens the POS interface and enters the event PIN + their name
  3. The app calls POST /api/teams/{teamId}/pos-events/{eventId}/helper-login
  4. A scoped access token is created (8-hour expiry) and stored in a cookie + localStorage
  5. Server-side validation uses @fyit/crouton-auth's scoped access:
// Auto-imported when extending @fyit/crouton-auth layer -- no import needed
// Internal path (for reference): @fyit/crouton-auth/server/utils/scoped-access
export default defineEventHandler(async (event) => {
  const access = await requireScopedAccess(event, 'pos-helper-token')
  // access.displayName, access.resourceId, access.organizationId, access.role
})
requireScopedAccess is auto-imported via Nitro when your app extends the @fyit/crouton-auth layer. The layer configures nitro.imports.dirs to include its server/utils/ directory. If you are not extending the layer and instead importing directly, note that @fyit/crouton-auth/server is not an exported path -- use the granular export @fyit/crouton-auth/server/utils/scoped-access instead.

Configuration

export default defineNuxtConfig({
  extends: [
    '@fyit/crouton',
    '@fyit/crouton-auth',
    '@fyit/crouton-sales',
    './layers/sales'
  ]
})

The package requires @fyit/crouton-core and @fyit/crouton-auth as peer dependencies. The @nuxtjs/i18n module is included automatically. For thermal printing, node-thermal-printer is an optional peer dependency.

i18n

The package ships translations for English (en), Dutch (nl), and French (fr). Translation keys are namespaced under sales.* (e.g., sales.cart.empty, sales.orders.title, sales.sidebar.events).