Event-based Point of Sale system for markets and pop-up events.
Status: Beta -- under active development. API may change.
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:
@fyit/crouton-auth)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)
]
})
Copy the JSON schema files from the package to your project's schemas/ directory:
cp node_modules/@fyit/crouton-sales/schemas/*.json ./schemas/
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.
npx nuxt db:generate
npx nuxt db:migrate
<script setup lang="ts">
const eventId = 'your-event-id'
</script>
<template>
<SalesClientOrderInterface :event-id="eventId" />
</template>
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?)
interface UsePosOrderOptions {
/** API base path for orders, defaults to '/api/sales/events' */
apiBasePath?: string
/** Whether to trigger print queue after checkout */
enablePrinting?: boolean
}
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()
interface HelperLoginOptions {
teamId: string
eventId: string
pin: string
helperName?: string // For new helpers
helperId?: string // For returning helpers
}
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.
All components are auto-imported with the Sales prefix.
Client/)| Component | Props | Description |
|---|---|---|
SalesClientOrderInterface | eventId (required), productsCollection?, categoriesCollection? | Main order page -- combines category tabs, product grid, cart sidebar, options modal, and mobile drawer |
SalesClientProductList | products | Product grid with inline option selection (single/multi-select) |
SalesClientCategoryTabs | categories, modelValue, productCounts | Category tab navigation with product counts |
SalesClientCart | items, total, disabled, clientRequired?, hasClient? | Shopping cart with quantity controls. Emits: updateQuantity, remove, checkout, clear |
SalesClientCartTotal | count, total, size? ('sm' or 'lg') | Order total display with animated item count badge |
SalesClientProductOptionsSelect | modelValue, options, multipleAllowed? | Product option picker (grid of selectable cards) |
SalesClientSelector | clients, 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 |
Pos/)| Component | Props | Description |
|---|---|---|
SalesPosOrdersList | eventId?, collectionName?, showReprint?, printApiBasePath?, refreshInterval? | Orders table with status filter tabs, auto-refresh toggle, and optional reprint button |
Admin/)| Component | Props | Description |
|---|---|---|
SalesAdminPosSidebar | basePath (required), showPrinters?, showHelpers?, additionalItems? | Vertical navigation menu for POS admin (events, products, categories, locations, clients) |
Settings/) -- Opt-in| Component | Props | Description |
|---|---|---|
SalesSettingsReceiptSettingsModal | modelValue, apiEndpoint | Modal for customizing receipt text (headers, footer, section titles) |
SalesSettingsPrintPreviewModal | modelValue, printer, testPrintApiBase, receiptSettings, locationName? | Receipt preview modal with visual preview and test print button |
The package provides JSON schemas for 10 collections. All use the sales prefix when generated.
| Schema | Table Name | Key Fields |
|---|---|---|
events.json | salesEvents | title, slug, startDate, endDate, status, isCurrent, helperPin |
products.json | salesProducts | eventId, categoryId, locationId, title, price, isActive, hasOptions, options (repeater) |
categories.json | salesCategories | eventId, title, displayOrder |
orders.json | salesOrders | eventId, clientId, clientName, eventOrderNumber, status, isPersonnel, overallRemarks |
orderItems.json | salesOrderitems | orderId, productId, quantity, unitPrice, totalPrice, remarks, selectedOptions |
locations.json | salesLocations | eventId, title |
clients.json | salesClients | title, isReusable |
eventSettings.json | salesEventsettings | eventId, settingKey, settingValue |
| Schema | Table Name | Key Fields |
|---|---|---|
printers.json | salesPrinters | eventId, locationId, title, ipAddress, port, showPrices, isActive |
printQueues.json | salesPrintqueues | eventId, orderId, printerId, status (0=pending, 1=printing, 2=done, 9=error), printData, printMode |
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.
@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
})
}
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.
helperPin field (up to 6 characters)POST /api/teams/{teamId}/pos-events/{eventId}/helper-login@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.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.
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).