Offline — orders save locally{{ offlineQueueCount ? ' · ' + offlineQueueCount + ' queued' : '' }}, syncing when you reconnect.
{{ offlineQueueCount }} offline order{{ offlineQueueCount > 1 ? 's' : '' }} pending sync

Order saved offline

It’ll sync automatically when you’re back online.

{{ offlineQueueCount }} order{{ offlineQueueCount > 1 ? 's' : '' }} queued locally

Offline Orders ({{ offlineQueueCount }})

No offline orders queued.

{{ o.itemCount }} item{{ o.itemCount > 1 ? 's' : '' }} · ${{ (o.total || 0).toFixed(2) }}

{{ o.payment?.method || 'unpaid' }} · {{ o.locationName }}

queued
Version {{ latestAvailableVersion }} is available

Taza POS

Taza

Taza POS

v{{ APP_VERSION }}

{{ loginError }}

{{ loginError }}

Drinks have no choices yet (milk, hot or iced, shots). They only appear after you set them up.

{{ qtyInCart(product.id) }} in order ${{ product.price.toFixed(2) }}

No products found

Current Order

#{{ currentOrderNumber }}

{{ item.name }} ${{ item.price.toFixed(2) }} each

Note: {{ item.note }}

{{ item.quantity }}
${{ (item.price * item.quantity).toFixed(2) }}
Subtotal${{ cartSubtotal.toFixed(2) }}
Resident (10%)-${{ residentDiscount.toFixed(2) }}
Discount-${{ manualDiscountAmount.toFixed(2) }}
Tax (11.5%)${{ cartTax.toFixed(2) }}
Total${{ cartTotal.toFixed(2) }}

Drinks have no choices yet (milk, hot or iced, shots). They only appear after you set them up.

Search Results

{{ selectedCategoryData.name }}

{{ registerView === 'categories' ? 'Categories' : 'Favorites' }}

{{ qtyInCart(product.id) }} in order ${{ product.price.toFixed(2) }}

No products found

Current Order

#{{ currentOrderNumber }}

{{ getCustomerDisplayName(selectedCustomer) }}

Resident (10% off)

Cart is empty

{{ item.name }}

${{ (item.price * item.quantity).toFixed(2) }}
${{ item.price.toFixed(2) }} each
{{ item.quantity }}

Note: {{ item.note }}

Subtotal ${{ cartSubtotal.toFixed(2) }}
Resident Discount (10%) -${{ residentDiscount.toFixed(2) }}
Discount -${{ manualDiscountAmount.toFixed(2) }}
Tax (11.5%) ${{ cartTax.toFixed(2) }}
Total ${{ cartTotal.toFixed(2) }}

Orders

Review and update orders{{ selectedLocation ? ' at ' + selectedLocation.name : '' }}.

{{ openTickets.length }} unpaid {{ openTickets.length === 1 ? 'ticket' : 'tickets' }}, ${{ openTicketsOwed.toFixed(2) }} still to collect. They are pinned to the top.

{{ ordersFilterLabel }}

{{ ordersRangeLabel }}{{ selectedLocation.name }}

#{{ order.order_number }}

{{ order.customer_name }}

{{ getOrderAge(order.created_at) }}

{{ order.order_type?.replace('_', ' ') }}

{{ order.status }} Unpaid ${{ parseFloat(order.total).toFixed(2) }} {{ order.payment_status }}
  • {{ l.quantity }} x {{ l.name }} Note: {{ l.note }}
{{ order.item_count }} items ${{ parseFloat(order.total).toFixed(2) }}
Order Customer Type Items Total Status Payment Actions

#{{ order.order_number }} VRS Check items

{{ getOrderAge(order.created_at) }}

{{ order.customer_name }} {{ order.delivery_name }} — {{ order.order_type?.replace('_', ' ') }} {{ order.item_count }} items ${{ parseFloat(order.total).toFixed(2) }} {{ order.status }} Unpaid {{ order.payment_status }}

{{ ordersEmptyTitle }}

Try another status or date to find orders.

Showing {{ ordersFilterWord }} orders for {{ ordersRangeLabel }}.

Products

{{ selectedProducts.length }} selected

Showing {{ filteredManageProducts.length }} of {{ products.length }} products

{{ productManageCategoryData.name }}
Product Category Price Shows on Status Actions

{{ product.name }}

{{ product.name_es }}

{{ parseFloat(product.inventory_stock || 0).toFixed(1) }} {{ product.inventory_unit }}
{{ product.category_name }} ${{ parseFloat(product.price).toFixed(2) }} {{ product.is_active ? 'Active' : 'Inactive' }}

Nothing in {{ productManageCategoryData.name }} yet.

No products in {{ productManageCategoryData.name }} match “{{ productManageSearch }}”.

No products found

Categories

Drag to reorder. Order affects display on POS.

⠿

{{ category.name }}

{{ category.name_es || '—' }}

{{ category.is_active ? 'Active' : 'Inactive' }}

Testing

Try the new things and say what happened. A "didn't work" with a note is the most useful thing you can give us.

Nothing to test right now.

{{ cl.name }}

{{ cl.environment }} Signed off

by {{ cl.signed_off_by_name || 'someone' }} on {{ formatShoppingDate(cl.signed_off_at) }}

{{ cl.pass }} worked · {{ cl.fail }} broken · {{ cl.untested }} not tried yet

{{ qaOpenChecklist.name }}

{{ qaOpenChecklist.intro }}

{{ qaOpenChecklist.pass }} worked · {{ qaOpenChecklist.fail }} broken · {{ qaOpenChecklist.untested }} not tried yet

{{ section.name }}

{{ item.title }}

Live Leaves a record

{{ item.expectation }}

{{ item.latest_result === 'pass' ? 'Worked' : (item.latest_result === 'fail' ? 'Broken' : 'Skipped') }} — {{ item.latest_by_name || 'someone' }}, {{ formatShoppingDate(item.latest_at) }} “{{ item.latest_note }}”
Who Check Result When
{{ h.by_name || 'Someone' }}

{{ h.title }}

“{{ h.note }}”

{{ h.result === 'pass' ? 'Worked' : (h.result === 'fail' ? 'Broke' : 'Skipped') }} {{ formatQaTime(h.created_at) }}

Things to Buy

Anyone can add. A manager marks it purchased.

🔍

Showing {{ filteredShoppingItems.length }} of {{ shoppingItems.length }}

Item {{ shoppingSortArrow('name') }} Where Asked for {{ shoppingSortArrow('created_at') }} Cost Status {{ shoppingSortArrow('status') }} Actions
Urgent

{{ item.name }}

{{ item.notes }}

{{ item.location_name || '—' }}

{{ item.requested_by_name || 'Someone' }}

{{ formatShoppingDate(item.created_at) }}

${{ parseFloat(item.actual_cost).toFixed(2) }} ~${{ parseFloat(item.estimated_cost).toFixed(2) }} — {{ item.status === 'open' ? 'Needed' : (item.status === 'purchased' ? 'Bought' : 'Dropped') }}

{{ item.purchased_by_name }}, {{ formatShoppingDate(item.purchased_at) }}

Manager only
🛒

Nothing on the list. When something breaks or runs out, add it here.

Nothing matches those filters.

Kitchen

Prepare batches, manage stock and wrap up the day.

Produce Batch

Make prepared items

Receive Stock

Log deliveries

Record Waste

Log spoiled or damaged items

Closing Count

End-of-day inventory check

Activity Log

Review kitchen activity

Select Recipe

No production recipes configured yet. Tap "Create Recipe" to add one.

{{ kitchenRecipeEditId ? 'Edit Recipe' : 'Create Recipe' }}

A batch increases this product's stock at the location where it's produced.

{{ ing._name }}

{{ ing._unit }}

{{ kitchenRecipeDetail.name }}

Makes {{ kitchenRecipeDetail.output_quantity }} per batch

Output product: {{ kitchenRecipeDetail.output_product.name }} ${{ parseFloat(kitchenRecipeDetail.output_product.price).toFixed(2) }}
No output product set — edit the recipe and pick one before producing.
Pick a location to see on-hand stock and produce a batch.

Ingredients needed at {{ selectedLocation.name }}

{{ ing.ingredient_name }} {{ (ing.quantity_required * kitchenBatchMultiplier).toFixed(2) }} {{ ing.unit }} (have {{ parseFloat(ing.quantity_on_hand).toFixed(2) }})
{{ kitchenBatchMultiplier }}

Record Waste

Closing Count

{{ item.name }}

Expected: {{ parseFloat(item.expected_quantity).toFixed(1) }} {{ item.unit }}

No prepared items to count.

Receive Stock

Activity Log

{{ entry.entry_type === 'batch' ? 'Batch Produced' : 'Waste Recorded' }} {{ entry.entry_type }}

{{ entry.item_name }}

By {{ entry.performed_by }}

{{ new Date(entry.entry_date).toLocaleDateString() }}

{{ new Date(entry.entry_date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }}

Exp: {{ new Date(entry.expires_at).toLocaleDateString() }}

No activity found{{ kitchenLogFilter !== 'all' ? ' for this filter' : '' }}.

⏰

TIME'S UP!

{{ bakingSession.steps[bakingSession.current_step - 1]?.title }}

Baking

{{ recipe.name }}

{{ recipe.output_quantity }} {{ recipe.output_unit }} of {{ recipe.output_item_name }}

Stock: {{ parseFloat(recipe.current_stock).toFixed(1) }}
{{ recipe.step_count }} steps {{ recipe.total_time }} min No steps yet

No production recipes found. Create recipes in the Kitchen section first.

{{ bakingSelectedRecipe.name }} — Steps

Total time: {{ bakingSelectedRecipe.total_time || 0 }} min · {{ (bakingSelectedRecipe.steps || []).length }} steps

{{ step.step_number }}

{{ step.title }}

{{ step.description }}

{{ step.step_type }} {{ step.duration_minutes }}m

No steps yet. Add steps below.

Add Step

{{ bakingSession.recipe_name }}

Batch: {{ bakingSession.batch_multiplier }}x

Step {{ bakingSession.current_step }} of {{ bakingSession.total_steps }} {{ bakingSession.steps[bakingSession.current_step - 1].step_type }}

{{ bakingSession.steps[bakingSession.current_step - 1].title }}

{{ bakingSession.steps[bakingSession.current_step - 1].description }}

{{ formatBakingTimer(bakingTimerSeconds) }}

{{ bakingSession.steps[bakingSession.current_step - 1].duration_minutes }} min total

Ingredients ({{ bakingSession.batch_multiplier }}x batch)
{{ ing.item_name }} {{ (ing.quantity_required * bakingSession.batch_multiplier).toFixed(2) }} {{ ing.unit }}

{{ (user.role === 'admin' || user.role === 'manager') ? 'My Shift Today' : "Today's Baking" }}

{{ bakingMyShift.status }}
{{ formatTime12h(bakingMyShift.start_time) }} • Shift: {{ Math.floor(bakingMyShift.total_budget_minutes / 60) }}h {{ bakingMyShift.total_budget_minutes % 60 }}m • Scheduled: {{ Math.floor(bakingMyShift.total_scheduled_minutes / 60) }}h {{ bakingMyShift.total_scheduled_minutes % 60 }}m
Start: {{ formatTime12h(bakingMyShift.start_time) }} Today's Baking List
{{ bakingMyShift.items_completed }} of {{ bakingMyShift.items_total }} items done
{{ idx + 1 }}. {{ item.recipe_name }} ({{ item.batch_multiplier }}x) {{ item.actual_minutes }}m {{ item.estimated_minutes }}m est Done Baking Pending
Recipe User Started Status Multiplier
{{ s.recipe_name }} {{ s.user_name }} {{ new Date(s.started_at).toLocaleString() }} {{ s.status }} {{ s.batch_multiplier }}x

No baking sessions yet.

Inventory

Products and ingredients across all locations

Showing {{ visibleStockRows.length }} of {{ invView === 'stock' ? stockMatrix.length : ingredientMatrix.length }}
Product {{ sortIndicator('product_name') }} Category {{ sortIndicator('category_name') }} {{ loc.name }} {{ sortIndicator('loc_' + loc.id) }} Total {{ sortIndicator('total') }} Actions

{{ row.product_name }} inactive

${{ parseFloat(row.price).toFixed(2) }}

{{ row.category_name || '—' }} {{ row.quantities[loc.id]?.quantity ?? 0 }} {{ row.total }}
No products yet. Add some via the Products tab. No products match the current filters.
Ingredient {{ sortIndicator('name') }} Unit {{ sortIndicator('unit') }} Supplier {{ sortIndicator('supplier') }} {{ loc.name }} {{ sortIndicator('loc_' + loc.id) }} Total {{ sortIndicator('total') }} Actions

{{ row.name }}

PAR {{ row.par_min }}–{{ row.par_target }}

{{ row.unit }} {{ row.supplier || '—' }} {{ formatQty(row.quantities[loc.id]?.quantity ?? 0, row.unit) }} {{ formatQty(row.total, row.unit) }}
No ingredients yet. Click "+ Add Ingredient" above. No ingredients match the current filters.
When Product From To Qty By
{{ new Date(t.transferred_at).toLocaleString() }} {{ t.product_name }} {{ t.from_location_name }} {{ t.to_location_name }} {{ t.quantity }} {{ [t.user_first_name, t.user_last_name].filter(Boolean).join(' ') || '—' }}
No transfers logged yet.
When Kind Item Location Action Before → After Δ By Notes
{{ new Date(a.created_at).toLocaleString() }} {{ a.entity_type }} {{ a.entity_name || ('#' + a.entity_id) }} {{ a.location_name || '—' }} {{ a.action_type }} {{ parseFloat(a.qty_before).toFixed(2) }} → {{ parseFloat(a.qty_after).toFixed(2) }} {{ parseFloat(a.qty_delta) > 0 ? '+' : '' }}{{ parseFloat(a.qty_delta).toFixed(2) }} {{ a.user_name || '—' }} {{ a.notes || '' }}
No adjustments logged yet.

{{ parReportSummary.critical || 0 }}

Critical

{{ parReportSummary.low || 0 }}

Low

{{ parReportSummary.ok || 0 }}

OK

Item On Hand PAR Min Target Order Qty Status

{{ item.name }}

{{ item.category_tag || '' }} {{ item.supplier ? '/ ' + item.supplier : '' }}

{{ parseFloat(item.quantity_on_hand).toFixed(1) }} {{ item.unit }} {{ parseFloat(item.par_min).toFixed(1) }} {{ parseFloat(item.par_target).toFixed(1) }} {{ item.suggested_order_qty > 0 ? parseFloat(item.suggested_order_qty).toFixed(1) + ' ' + item.unit : '—' }} ({{ item.suggested_purchase_units }} {{ item.purchase_unit }}s) {{ item.status }}

{{ transferMode === 'get' ? 'Get From Main' : 'Send Back to Main' }}

{{ transferMode === 'get' ? 'Move stock from Main to ' + (selectedLocation?.name || 'here') + '.' : 'Move stock from ' + (selectedLocation?.name || 'here') + ' back to Main.' }}

Loading... No products match "{{ transferSearch }}" Nothing available to transfer.

{{ row.product_name }}

{{ row.kind }} · Source has {{ row.source_qty }} · already at dest: {{ row.dest_qty }}

Adjust stock

{{ adjustForm.type === 'product' ? 'Product' : 'Ingredient' }}: {{ adjustForm.name }}

Current: {{ formatQty(adjustCurrentQty, adjustForm.unit) }} · After: {{ formatQty(adjustPreview, adjustForm.unit) }}

Restock {{ restockTarget.name }}

On hand: {{ restockTarget.quantity_on_hand }} {{ restockTarget.unit }}

{{ confirmConfig.title }}

{{ confirmConfig.message }}

Add a Time Entry

For someone who never punched in. This is recorded in the audit log under your name.

Leave empty only if they are still on the clock right now.

{{ addTimeEntryError }}

{{ scheduleForm.id ? 'Edit Shift' : 'Add a Shift' }}

Rota weeks run Friday to Thursday, the same as a pay period. No more than {{ SCHEDULE_MAX_CONCURRENT }} people on at the same time.

No staff loaded yet.

{{ scheduleDateIssue }}

This shift is on a week already worked. You can correct it here or delete it — but moving it to another day means picking one inside the open rota{{ scheduleEditableRange.min ? ', ' + scheduleEditableRange.label : '' }}.

That is a fourth person at {{ formatTime12h(scheduleConflict.at) }}

Already on: {{ scheduleConflict.names.join(', ') }}. Only {{ SCHEDULE_MAX_CONCURRENT }} can be on at once, so saving this will be rejected — move a time or drop someone first.

{{ schedulePersonConflict.name }} is already on at that time

Their existing shift that day runs {{ formatTime12h(schedulePersonConflict.start) }} – {{ formatTime12h(schedulePersonConflict.end) }} at {{ schedulePersonConflict.location }}. Nobody can be booked twice over the same hours, so saving this will be rejected — change the times, or edit that shift instead of adding a second one.

{{ scheduleError }}

{{ ingredientForm.id ? 'Edit' : 'Add' }} Ingredient

Customers

Customer Contact Resident Orders Visits Total Spent Actions

{{ getCustomerDisplayName(customer) }}

{{ customer.first_name }} {{ customer.last_name }}

{{ customer.email || '—' }}

{{ formatDisplayPhone(customer.phone) || '—' }}

— 0
{{ customer.loyalty_visits }} visits {{ customer.loyalty_rewards_lifetime }} earned

Free item earned!

{{ customer.loyalty_visits_month || 0 }} this month

—
${{ parseFloat(customer.total_spent || 0).toFixed(2) }}

Dashboard

Today's Revenue

${{ dashboardData?.today?.total_revenue?.toFixed(2) || '0.00' }}

Orders Today

{{ dashboardData?.today?.total_orders || 0 }}

Active Orders

{{ dashboardData?.active_orders || 0 }}

Low Stock Items

{{ dashboardData?.low_stock_count || 0 }}

Top Products Today

{{ index + 1 }} {{ product.product_name }}

{{ product.quantity }} sold

${{ parseFloat(product.revenue).toFixed(2) }}

AI Insights

AI-powered business intelligence

API Key Required

Add your Anthropic API key in Settings to enable AI insights.

Latest Insight - {{ aiLatestInsight.insight_date }}

{{ aiLatestInsight.insight_type }} analysis | Confidence: {{ Math.round((aiLatestInsight.confidence_score || 0) * 100) }}%

{{ aiLatestInsight.tokens_used?.toLocaleString() }} tokens
${{ Number(aiLatestInsight.cost_usd).toFixed(4) }}
~${{ (aiLatestInsight.tokens_used * 0.000006).toFixed(4) }}

{{ aiLatestInsight.full_analysis?.summary || aiLatestInsight.summary }}

{{ aiLatestInsight.model_used }} In: {{ aiLatestInsight.input_tokens?.toLocaleString() }} Out: {{ aiLatestInsight.output_tokens?.toLocaleString() }} {{ (aiLatestInsight.generation_time_ms / 1000).toFixed(1) }}s

Sales Analysis

{{ aiLatestInsight.full_analysis?.sales_analysis?.overview }}

Trends

  • • {{ trend }}

Peak Hours

{{ h }}

Weather Impact

{{ aiLatestInsight.full_analysis?.weather_correlation?.impact }}

  • • {{ p }}

Product Recommendations

Feature These

{{ p }}

Consider Promoting

{{ p }}

Watch List

{{ p }}

Action Items

{{ item.priority }}

{{ item.action }}

{{ item.reason }}

No action items

Anomalies Detected

  • {{ a }}

Forecast & Preparations

{{ aiLatestInsight.full_analysis.forecasting.outlook }}

  • • {{ p }}

No Insights Yet

Generate your first AI insight to get started with business intelligence.

Business Notes

Notes are included as context for AI analysis (events, promotions, weather notes, etc.)

No business notes yet. Add notes to give the AI context about your business.

{{ note.title }}

{{ note.note_type }}

{{ note.content }}

{{ note.note_date }} | by {{ note.author_first_name || 'System' }}

Loyalty Program

Manage rewards rules and view reports

Enrolled

{{ loyaltyReport.enrolled_customers }}

Active This Month

{{ loyaltyReport.active_this_month }}

Rewards Issued

{{ loyaltyReport.rewards_issued_this_month }}

Redeemed ($)

${{ (loyaltyReport.redemption_value_this_month || 0).toFixed(2) }}

Top Visitors

{{ v.first_name }} {{ v.last_name }} ({{ v.total_visits }} visits)
Rule Type Reward Active Actions

{{ rule.name }}

{{ rule.reward_description }}

Every {{ rule.trigger_count }} visits Surprise ({{ (rule.surprise_probability * 100).toFixed(0) }}%) Category ({{ rule.trigger_count }}) {{ rule.rule_type }} Free Item ${{ parseFloat(rule.reward_value).toFixed(2) }} off {{ rule.reward_value }}% off {{ rule.is_active ? 'Yes' : 'No' }}

No loyalty rules configured.

Settings

Vieques Room Service link

VRS sends its delivery orders here, signed with a shared secret. A secret is set. No secret yet, so VRS orders are refused.

Try the delivery light

Sends a pretend delivery to the location you are working at. Then open the register there: the Delivery light comes on within 15 seconds. Open it and press Start this delivery to turn it off. This only works on beta and local, never on live.

Copy this now. It is shown once and cannot be read again.

{{ vrsSecretShown }}

Then give it to VRS as its TAZA_SECRET without pasting it into chat. Making a new one stops the old one working.

Payroll Deductions

Everything is off until you switch it on, and nothing is withheld from anyone while it is off. These rates are yours to confirm with the accountant — they are starting points, not advice. Employer rows are recorded for cost reporting and never come off anyone's pay.

Nothing loaded yet — tap Reload.
{{ d.label }} employer
{{ d.notes }}
{{ d.calc_method === 'fixed' ? '$' : '%' }}

Per-employee — set each person's rate on their staff record from their Form 499 R-4.1, not here. Switch this on to include the line on stubs.

Changing a rate only affects payroll generated after the change. Stubs already issued keep the rate that was in force when they were run.

Business Information

POS Settings

Sales Tax (State + Municipality)

Each order line is taxed by its category's bracket. State rate varies; municipality rate applies to everything.

Categories auto-suggest a bracket from their name when you create them (e.g., "Beer & Seltzers" → Alcohol). You can override per category.

Order Time Alerts

Order age color: green → yellow (warning) → red (urgent)

Payment Methods

Cash, Card, Venmo and ATH Móvil are always available at the register. Card means a payment you took on your own reader or phone; Taza records it on the order.

AI Insights

Required for AI insights. Get one at console.anthropic.com

Optional. Enables weather correlation analysis.

{{ step.status === 'pass' ? 'Pass' : step.status === 'skip' ? 'Skip' : 'FAIL' }} {{ key.replace(/_/g, ' ') }} {{ step.detail || step.version || '' }}{{ step.time_s ? ' (' + step.time_s + 's)' : '' }}

Locations

No locations yet.
{{ loc.name }} Inactive Kitchen Selected
/menu?loc={{ loc.slug }}{{ loc.address ? ' · ' + loc.address : '' }}

{{ locationForm.id ? 'Edit Location' : 'Add Location' }}

/menu?loc=slug

Account Settings

Change Password

Change PIN

Stock at {{ selectedLocation?.name || 'this location' }}

What you have on hand right now.

Pick a location first.
Product On Hand

{{ row.product_name }}

{{ row.category_name || '' }}

{{ row.quantity }}
Loading… No products match "{{ myStockSearch }}" Nothing in stock at this location yet.

My account

Your profile, location and workspace preferences.

{{ userDisplayName }}

{{ user?.email }}

{{ user?.role }}

Working location

{{ selectedLocation?.name || 'No location selected' }}

Choose the location you're working at.

Your PIN

{{ myPin.pin }}

{{ myPin.reason }}

Your PIN is used to quickly sign in at the terminal.

Version {{ APP_VERSION }}

Register layout

How the register shows products on this device. Each person keeps their own choice.

Sound effects

Button clicks and the finish chime on this device.

Order screen cues

Make orders readable from across the counter. Kept on this device.

Color the choices

HOT in red with a flame, ICED in blue with a cube, "No tomato" in red with a ban sign.

Item pictures

Icons on a color tint are the default and are quicker to read. Photos show uploaded product pictures instead.

Faint ingredient pictures Experiment

A very light picture behind an order line (lavender, coconut, coffee...). Try it and tell us if it helps.

Workspace theme

Choose a look for your POS.

Dark cards

Dark page titles and section labels with light text.

Applies to your workspace appearance.

Time Clock

{{ timeClockElapsed || '00:00:00' }}

Clocked in since {{ new Date(timeClockStatus.entry?.clock_in_at).toLocaleTimeString() }}

--:--:--

Not clocked in

This week total {{ Number(timeClockSummary.total_hours || 0).toFixed(1) }}h
Date Clock In Clock Out Hours Status
{{ new Date(entry.clock_in_at).toLocaleDateString() }} {{ new Date(entry.clock_in_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) }} {{ entry.clock_out_at ? new Date(entry.clock_out_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : '—' }} {{ entry.clock_out_at ? (((new Date(entry.clock_out_at) - new Date(entry.clock_in_at)) / 3600000) - (entry.break_minutes || 0) / 60).toFixed(1) + 'h' : '—' }} {{ entry.status === 'closed' ? 'Ready to Review' : entry.status }}
No entries this week
${{ myPaySummary.last_payment_amount != null ? Number(myPaySummary.last_payment_amount).toFixed(2) : '0.00' }}
Last Paid
${{ Number(myPaySummary.since_last_payroll?.estimated || 0).toFixed(2) }}
Since Last Payroll
{{ Number(myPaySummary.since_last_payroll?.hours || 0).toFixed(1) }}h
Unpaid Hours
${{ Number(myPaySummary.pay_rate || 0).toFixed(2) }}/hr
Pay Rate

{{ myPayView === 'mtd' ? 'Month to Date' : 'Year to Date' }}

{{ Number(myPaySummary[myPayView]?.hours || 0).toFixed(1) }}h
Hours Worked
${{ Number(myPaySummary[myPayView]?.paid || 0).toFixed(2) }}
Total Paid
${{ Number(myPaySummary[myPayView]?.estimated || 0).toFixed(2) }}
Total Earned (Est.)

Upcoming Payroll

Period Hours Rate Est. Gross Status
{{ item.start_date }} — {{ item.end_date }} {{ Number(item.hours_worked).toFixed(1) }}h ${{ Number(item.rate).toFixed(2) }}/hr ${{ Number(item.gross_amount).toFixed(2) }} due
Since last payroll {{ Number(myPaySummary.since_last_payroll?.hours || 0).toFixed(1) }}h ${{ Number(myPaySummary.pay_rate || 0).toFixed(2) }}/hr ${{ Number(myPaySummary.since_last_payroll?.estimated || 0).toFixed(2) }} estimate
No hours since last payroll

Payment History

Period Amount Method Reference Date
{{ row.start_date }} — {{ row.end_date }} ${{ Number(row.amount).toFixed(2) }} {{ row.method || '—' }} {{ row.reference || '—' }} {{ row.date ? new Date(row.date).toLocaleDateString() : '—' }}
No payments recorded yet
{{ timeClockFilteredEntries.length }} of {{ timeClockAllEntries.length }} entries
Staff Where Date In Out Hours Status Actions
{{ entry.nickname || entry.first_name }} {{ entry.location_name || '—' }} {{ new Date(entry.clock_in_at).toLocaleDateString() }} {{ new Date(entry.clock_in_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) }} {{ entry.clock_out_at ? new Date(entry.clock_out_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : '—' }} {{ entry.clock_out_at ? (((new Date(entry.clock_out_at) - new Date(entry.clock_in_at)) / 3600000) - (entry.break_minutes || 0) / 60).toFixed(1) + 'h' : '—' }} {{ timeEntryStatusLabel(entry) }}
No entries to review

Edit Time Entry

Payroll

Period Employees Total Gross Total Paid Status Actions
{{ period.start_date }} — {{ period.end_date }} {{ period.employee_count || 0 }} ${{ Number(period.total_gross || 0).toFixed(2) }} ${{ Number(period.total_paid || 0).toFixed(2) }} {{ period.status }}
No pay periods yet

{{ payrollSelectedPeriod.start_date }} — {{ payrollSelectedPeriod.end_date }}

Created by {{ payrollSelectedPeriod.created_by_name }}

{{ payrollSelectedPeriod.status }}

{{ payrollWarnings.length }} thing{{ payrollWarnings.length === 1 ? '' : 's' }} to look at

Nothing below was added or deducted automatically. Decide each one, adjust the hours if needed, then re-generate.

  • {{ w.employee }} — {{ w.date }} — {{ w.message }}
Employee Hours Rate Gross Deductions Net Notes Status Actions
{{ item.nickname || item.first_name }} {{ item.last_name || '' }} ${{ Number(item.rate).toFixed(2) }}/hr ${{ Number(item.gross_amount).toFixed(2) }} −${{ Number(item.deductions_total).toFixed(2) }} —
{{ di ? ' · ' : '' }}{{ d.label }}
${{ Number(item.net_amount || item.gross_amount).toFixed(2) }}
Net is higher than gross. Edit and save this item again, or Start Over.
{{ item.notes || '' }} {{ item.status }}
No items. Click Generate to create payroll.

Summary

{{ payrollSelectedPeriod.summary.total_employees }}
Employees
{{ payrollSelectedPeriod.summary.total_hours }}
Total Hours
{{ Number(payrollSelectedPeriod.summary.total_overtime_hours || 0).toFixed(1) }}
Premium Hours
${{ Number(payrollSelectedPeriod.summary.total_gross).toFixed(2) }}
Gross
−${{ Number(payrollSelectedPeriod.summary.total_deductions || 0).toFixed(2) }}
Deductions
${{ Number(payrollSelectedPeriod.summary.total_net ?? payrollSelectedPeriod.summary.total_gross).toFixed(2) }}
Net Pay
Paid ${{ Number(payrollSelectedPeriod.summary.total_paid).toFixed(2) }} · ${{ Number(payrollSelectedPeriod.summary.total_remaining).toFixed(2) }} remaining

{{ payrollSelectedPeriod.status === 'processing' ? 'Employee Signature' : 'Manager Signature' }}

{{ payrollPreflight.summary?.staff_count || 0 }} staff checked

The readiness check did not answer. Nothing is blocked — but look over the hours yourself before generating.

These hours cannot be priced correctly yet. Fix each one, then Re-check.

  • {{ c.user_name }} — {{ preflightReason(c.kind) }} — {{ c.detail }}

Not blocking. Nothing below was changed automatically.

  • {{ c.user_name }} — {{ preflightReason(c.kind) }} — {{ c.detail }}
Verified & Locked

Manager: {{ payrollSelectedPeriod.verified_by_name }}

Finalized: {{ new Date(payrollSelectedPeriod.finalized_at).toLocaleString() }}

Employee
Manager

Activity Log

{{ new Date(entry.created_at).toLocaleString() }} {{ entry.nickname || entry.first_name || 'System' }} {{ entry.details || entry.action }}

New Pay Period

Creates period for the current week (Monday - Sunday).

Record Payment

{{ payrollPayingItem.nickname || payrollPayingItem.first_name }} — net ${{ Number(payrollPayingItem.net_amount || payrollPayingItem.gross_amount).toFixed(2) }}

Schedule

No schedule window available.

This week has already been worked

Shifts on days payroll has not closed can still be corrected or removed here. New shifts go on the open rota — {{ scheduleEditableRange.label }}, never on a week already behind us.

Payroll has closed every day of this week

Nothing can be added, changed or removed here while it is closed. Reopen the pay period if this week still needs work on it.

{{ scheduleWeekLock.allLocked ? 'This whole week is locked' : 'Part of this week is locked' }}

Payroll has been closed for {{ scheduleWeekLock.label }}, so {{ scheduleWeekLock.allLocked ? 'every day of this week is read-only' : 'those days are read-only — the rest of the week can still be changed' }}. Reopen that pay period in Payroll if a locked day has to change.

{{ scheduleWeekLabel(scheduleActiveWeek) }}

Showing {{ scheduleVisibleCount }} of {{ scheduleWeekCount }} shift{{ scheduleWeekCount === 1 ? '' : 's' }} · {{ scheduleEmptyDayCount }} day{{ scheduleEmptyDayCount === 1 ? '' : 's' }} with nobody on

Loading the rota…

{{ day.weekday }} {{ day.pretty }} Today Locked

{{ scheduleStaffFilter ? 'Not scheduled' : 'Nobody was on this day' }}

Nobody on this day

Not scheduled

{{ day.shifts.length }} shift{{ day.shifts.length === 1 ? '' : 's' }} · peak {{ day.peak.count }} of {{ SCHEDULE_MAX_CONCURRENT }} people at the same time

Full from {{ formatTime12h(day.peak.at) }} — {{ day.peak.names.join(', ') }} are all on. A fourth person will be rejected.

{{ s.user_name }}

{{ formatTime12h(s.start_time) }} – {{ formatTime12h(s.end_time) }} · {{ s.location_name }}

{{ s.note }}

Locked
Pick a week above to see the rota.

Expenses

Total

${{ Number(expenseReport.totals?.total || 0).toFixed(2) }}

Count

{{ expenseReport.totals?.count || 0 }}

{{ cat.category }}

${{ Number(cat.total).toFixed(2) }}

Date Vendor Category Amount Method Actions
{{ exp.expense_date }} {{ exp.vendor || '—' }} {{ exp.category }} ${{ Number(exp.amount).toFixed(2) }} {{ exp.payment_method }}
No expenses found

{{ editingExpense ? 'Edit' : 'Add' }} Expense

AI-scanned receipt data — review and adjust before saving

Till Count

{{ tillSelectedSession.session_date }} — {{ tillSelectedSession.shift }}

{{ tillSelectedSession.status }}

Starting Cash

${{ Number(tillSelectedSession.starting_cash || 0).toFixed(2) }}

${{ Number(tillSelectedSession.pos_cash_sales_total || 0).toFixed(2) }}
${{ Number(tillSelectedSession.cash_drop_amount || 0).toFixed(2) }}

Denomination Count

{{ d.label }}
{{ d.qty }}
${{ (d.value * d.qty).toFixed(2) }}
Total: ${{ tillDenominations.reduce((sum, d) => sum + d.value * d.qty, 0).toFixed(2) }}

Paid Outs

{{ po.time }} — {{ po.description }} ({{ po.employee_initial }})
${{ Number(po.amount).toFixed(2) }}

Receipt Paid Outs

{{ rpo.receipt_store }} — {{ rpo.description }}
${{ Number(rpo.amount).toFixed(2) }}

Notes

{{ tillSelectedSession.notes || 'No notes' }}

Summary

Starting Cash
${{ Number(tillSelectedSession.starting_cash || 0).toFixed(2) }}
+ POS Cash Sales
${{ Number(tillSelectedSession.pos_cash_sales_total || 0).toFixed(2) }}
- Cash Drops
${{ Number(tillSelectedSession.cash_drop_amount || 0).toFixed(2) }}
- Paid Outs
${{ Number(tillSelectedSession.paid_outs_total || 0).toFixed(2) }}
- Receipt Paid Outs
${{ Number(tillSelectedSession.receipt_paid_outs_total || 0).toFixed(2) }}
Expected Cash
${{ Number(tillSelectedSession.expected_cash_in_drawer || 0).toFixed(2) }}
Actual Cash (denominations)
${{ tillDenominations.reduce((sum, d) => sum + d.value * d.qty, 0).toFixed(2) }}
Over/Short {{ tillSelectedSession.over_short_status || (Number(tillSelectedSession.over_short_amount) > 0 ? 'OVER' : Number(tillSelectedSession.over_short_amount) < 0 ? 'SHORT' : 'EVEN') }}
${{ Number(tillSelectedSession.over_short_amount || 0).toFixed(2) }}

{{ tillSelectedSession.status === 'open' ? 'Employee Signature' : 'Manager Signature' }}

Employee signature Manager signature

Verified by {{ tillSelectedSession.verified_by_name || 'Manager' }}

{{ tillSelectedSession.finalized_at ? new Date(tillSelectedSession.finalized_at).toLocaleString() : (tillSelectedSession.locked_at ? new Date(tillSelectedSession.locked_at).toLocaleString() : '') }}

Employee

Employee signature

Manager

Manager signature

Awaiting Manager Verification

Submitted {{ tillSelectedSession.submitted_at ? new Date(tillSelectedSession.submitted_at).toLocaleString() : '' }}

Employee Signature

Employee signature

No active till session for today

Date Shift Opened By Over/Short Status Actions
{{ session.session_date }} {{ session.shift?.replace('_', ' ') }} {{ session.nickname || session.first_name || '—' }} ${{ Number(session.over_short_amount || 0).toFixed(2) }} {{ session.status }}
No till sessions found

New Till Session

Console · v{{ APP_VERSION }}

{{ user?.nickname || user?.first_name }} — {{ user?.role }}

Your workspace

{{ selectedLocation ? selectedLocation.name : 'No location selected' }}{{ user?.role }}

Pick a location to see what you can do here.

No apps are enabled for your account at this location.

{{ group.title }}

Stock items

Everything you count: ingredients, supplies, prepared goods and goods you resell. Ingredients stay in the kitchen.

Showing {{ siRows.length }} of {{ si.items.length }}

Ingredients are kept in the kitchen only, so this location holds none.

ItemKindOn hand ParStatusActions
{{ i.name }}inactive {{ i.kind }} {{ siOnHand(i) }} {{ i.unit }} {{ Number(i.par_min) || '' }} Low
{{ si.loading ? 'Loading...' : 'Nothing matches.' }}

New to this screen?

Take a short tour of {{ (TOUR_TOPICS.find(t => t.id === tour.offer) || {}).title }}. It shows what each part does, one step at a time. You can stop whenever you like.

Stock model (preview)

Phase 1 of the inventory redesign. This copies the old inventory tables into the new model. Staff do not see or use it yet.

Sales and the new stock

{{ ss.status.enabled ? 'Sales take from the new stock' : 'Sales take from the old counts' }}

A paid order takes stock from the new stock using each product's list (What one sale uses). It never stops a sale, never goes below zero (it takes what is there and notes the shortfall), and takes stock once per order. A void does not put stock back, same as before.

  • Items in the new stock: {{ ss.status.stock_items }} · the stock model has not been applied yet (use Check, then Apply, below)
  • Counted products with no list of what one sale uses: {{ ss.status.tracked_without_list }} (the new stock would stop counting them: {{ ss.status.tracked_without_list_names.slice(0, 6).join(', ') }}, ...)
  • Orders handled by the new stock so far: {{ ss.status.orders_applied }}, of which came up short: {{ ss.status.orders_short }}

If the last {{ ss.preview.orders_checked }} paid orders were rung up against today's counts: {{ ss.preview.orders_that_would_come_up_short }} would come up short, and {{ ss.preview.orders_with_nothing_to_take }} have nothing to take (no list on their products). Nothing was changed.

ItemUsed by these salesOn hand nowResult
{{ i.name }} ({{ i.kind }}) {{ i.used }} {{ i.unit }} {{ i.held_now }} Not held hereWould run outEnough
None of the recent orders used anything on a product list yet.

A check does all the work and then undoes it. Apply is only offered after a check that reconciles.

In the new model now: {{ stockModel.status.stock_items }} items, {{ stockModel.status.stock_levels }} counts, {{ stockModel.status.stock_movements }} ledger rows, {{ stockModel.status.product_components }} product links. Last applied {{ stockModel.status.last_run.ran_at }}. Never applied.

Report

{{ stockModel.report.reconciles ? 'Reconciles' : 'Does not reconcile' }} {{ stockModel.report.mode }}

Would create: {{ stockModel.report.created.items }} items, {{ stockModel.report.created.levels }} counts, {{ stockModel.report.created.components }} product links, {{ stockModel.report.created.movements }} ledger rows. Re-synced: {{ stockModel.report.refreshed_levels }}.

  • {{ c.check }} ({{ c.detail }})

For a person to look at

{{ key.replace(/_/g, ' ') }} {{ Array.isArray(list) ? list.length : list }}
  • {{ typeof line === 'string' ? line : JSON.stringify(line) }}

Nothing to review.

Showing {{ stockModelItems.length }} of {{ stockModel.items.length }}

ItemKindUnit TotalCounts by locationUsed by
{{ i.name }}inactive {{ i.kind }} {{ i.unit }} {{ i.total }} {{ stockLevelsText(i) }} {{ i.used_by_products }}
Nothing yet. Run a check, then apply.

Options

The choices a drink or sandwich offers. Build a group once, then say which categories or products use it.

Showing {{ ogShown.length }} of {{ optionGroups.length }}

Group Picks Options Used by Actions

{{ g.name }}

Hidden from the register

{{ ogPicksText(g) }} {{ o.name }} ({{ o.price_delta > 0 ? '+' : '-' }}${{ Math.abs(o.price_delta).toFixed(2) }}), and {{ g.options.length - 6 }} more None yet {{ ogUsedByText(g) }}

Loading option groups.

No option groups yet. Start with Milk or Temperature: press New group.

No groups match that search.

Problem reports

What staff reported from the app, newest and unhandled first.

Leave blank to keep them in this inbox only.

When someone sends a red box that says "Reference: ERR_...", type it here to see what actually went wrong.

What went wrong
{{ errLook.result.what }}
When
{{ errLook.result.when }}
Request
{{ errLook.result.request }}

{{ errLook.result.why }}

Showing {{ bugShown.length }} of {{ bugReports.length }}

When Who Where What happened Status Actions
{{ bugWhen(r) }} {{ r.user_name || 'Unknown' }}{{ r.user_role }} {{ r.location_name || 'No location' }}{{ r.screen }} · v{{ r.app_version }} · {{ r.environment }} {{ r.message }} {{ BUG_STATUS[r.status]?.label || r.status }} {{ r.files.length }}
{{ bugReportsLoading ? 'Loading...' : 'Nothing here. No problems reported.' }}
Picture sent with the report

Staff Management

Name Role PIN Status Last Login Actions
{{ (member.nickname || member.first_name || 'U').charAt(0).toUpperCase() }}

{{ member.nickname || (member.first_name + ' ' + member.last_name) }}

{{ member.email }}

{{ member.role }}
Active Inactive {{ timeSince(member.last_login) }}
No staff members found
Loading…
No events match this filter.
When Who Action IP
{{ formatFullTimestamp(ev.created_at) }} {{ ev.user_name || (ev.user_id ? ('User #' + ev.user_id) : '—') }} {{ loginActionLabel(ev.action) }} {{ activityTarget(ev) }} {{ ev.ip_address || '—' }}

Invite New Staff

Generate an invite code that new staff can use to create their account.

Share this code with the new staff member:

{{ generatedInviteCode }}

Valid for 7 days. They can use this on the login page.

Suggestion Box

No open suggestions
{{ s.nickname || s.first_name }} {{ new Date(s.created_at).toLocaleString() }}

{{ s.message }}

No reviewed suggestions
{{ s.nickname || s.first_name }} {{ new Date(s.created_at).toLocaleString() }}

{{ s.message }}

Reply by {{ s.replied_nickname || s.replied_first_name }} — {{ s.replied_at ? new Date(s.replied_at).toLocaleString() : '' }}

{{ s.admin_reply }}

Submit a Suggestion

Replies to your suggestions:

Your suggestion: {{ n.message.length > 80 ? n.message.slice(0, 80) + '...' : n.message }}

{{ n.admin_reply }}

Get Stock from Main

Pick how many of each you're taking to {{ selectedLocation?.name || 'this location' }}.

Loading…
No items are shared by Main yet. Ask an admin to flag items as "Share with other locations" from the inventory editor.
No items match "{{ borrowSearch }}"

{{ it.name }}

Main has {{ parseFloat(it.owner_quantity).toFixed(it.unit === 'each' ? 0 : 1) }} {{ it.unit }} · You have {{ parseFloat(it.borrowed_quantity).toFixed(it.unit === 'each' ? 0 : 1) }}

{{ editingStaff ? 'Edit Staff' : 'Add Staff' }}

A new PIN will be assigned and shown after saving.

A unique PIN will be auto-generated and shown after creating the staff member.

Sets the overtime rate

From their 499 R-4.1

Hired before 26 Jan 2017 — keeps the pre-reform 2× rate on daily overtime.

Daily overtime at 1.5×.

No hire date — overtime will default to 1.5×. Set it before the first real payroll run.

Apply everywhere
Per location
Copy from:
Select a location tab above to edit its permissions.

{{ pinResult.title }}

{{ pinResult.name }}

PIN

{{ pinResult.pin }}

Complete Payment

Order #{{ payingOrder.order_number }}

Payment received

Change due to customer

${{ lastChangeDue.toFixed(2) }}

Order #{{ selectedOrder?.order_number }}

{{ selectedOrder?.created_at }}

Test delivery. Not a real order.

Delivery from Vieques Room Service {{ selectedOrder.external_ref }}

For {{ selectedOrder.delivery_name }} · {{ selectedOrder.scheduled_for }}

Phone {{ selectedOrder.delivery_phone }}

Open drop-off on the map

One or more items are not on the Taza menu. Check the items with VRS before making them.

{{ selectedOrder.customer_name }}

Vieques Resident

No customer attached

Status

{{ selectedOrder.status }}

Payment

{{ selectedOrder.payment_status }}

Type

{{ selectedOrder.order_type?.replace('_', ' ') }}

Items

{{ item.product_name }}

Note: {{ item.special_instructions }}

{{ item.quantity }} x ${{ parseFloat(item.unit_price).toFixed(2) }}

${{ parseFloat(item.subtotal).toFixed(2) }}

Subtotal ${{ parseFloat(selectedOrder.subtotal).toFixed(2) }}
Discount -${{ parseFloat(selectedOrder.discount_amount).toFixed(2) }}
Loyalty Reward -${{ parseFloat(selectedOrder.loyalty_discount_amount).toFixed(2) }}
Tax ${{ parseFloat(selectedOrder.tax_amount).toFixed(2) }}
Total ${{ parseFloat(selectedOrder.total).toFixed(2) }}

{{ editingShoppingItem ? 'Edit item' : 'Add to the list' }}

Mark as purchased

{{ purchasingShoppingItem?.name }}

Choose an icon

Pick from the groups below.

{{ editingCategory ? 'Edit Category' : 'Add Category' }}

{{ categoryForm.name || 'Category Name' }}

{{ categoryForm.name_es || 'Nombre de Categoría' }}

Shown on the register's category tiles. JPG, PNG or WebP, max 2MB.

Quick Add Customer

{{ editingCustomer ? 'Edit Customer' : 'Add Customer' }}

If set, this will display instead of their name

{{ emailError }}

{{ phoneError }}

{{ editingInventory ? 'Edit Inventory Item' : 'Add Inventory Item' }}

Inventory Details

Add Inventory

{{ selectedLocation ? selectedLocation.name : 'All Locations' }}

Loading products...

{{ catName }}

{{ row.product_name }}

Current: {{ parseFloat(row.current_stock).toFixed(0) }}

No additions recorded yet.

{{ entry.product_name || entry.item_name }}

{{ entry.notes }}

+{{ entry.quantity_change }}

{{ new Date(entry.created_at).toLocaleDateString() }} {{ new Date(entry.created_at).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) }}

Order History

{{ viewingCustomer?.nickname || (viewingCustomer?.first_name + ' ' + viewingCustomer?.last_name) }}

No orders found

#{{ order.order_number }}

{{ new Date(order.created_at).toLocaleDateString() }} at {{ formatTime(order.created_at) }}

{{ order.item_count }} items

${{ parseFloat(order.total).toFixed(2) }}

{{ order.status }}
Total Orders: {{ viewingCustomer?.total_orders || 0 }} Lifetime Spent: ${{ parseFloat(viewingCustomer?.total_spent || 0).toFixed(2) }}

{{ editingLoyaltyRule ? 'Edit' : 'Add' }} Loyalty Rule

{{ loyaltyRewardProduct.name }} costs ${{ parseFloat(loyaltyRewardProduct.price).toFixed(2) }} but max reward is ${{ parseFloat(loyaltyRuleForm.max_reward_value).toFixed(2) }}. Customer pays the ${{ (parseFloat(loyaltyRewardProduct.price) - parseFloat(loyaltyRuleForm.max_reward_value)).toFixed(2) }} difference. {{ loyaltyRewardProduct.name }} (${{ parseFloat(loyaltyRewardProduct.price).toFixed(2) }}) is fully covered.

{{ editingNote ? 'Edit Note' : 'Add Business Note' }}

{{ toast.message }} tap to copy copied!