No products found
Current Order
#{{ currentOrderNumber }}{{ item.name }}
${{ item.price.toFixed(2) }} each
Search Results
{{ selectedCategoryData.name }}
Favorites
No products found
Current Order
#{{ currentOrderNumber }}{{ getCustomerDisplayName(selectedCustomer) }}
🏠 Resident (10% off)
Cart is empty
{{ item.name }}
${{ item.price.toFixed(2) }} each
Orders
#{{ order.order_number }}
👤 {{ order.customer_name }}
{{ getOrderAge(order.created_at) }}
{{ order.order_type?.replace('_', ' ') }}
| Order | Customer | Type | Items | Total | Status | Payment | Actions |
|---|---|---|---|---|---|---|---|
|
#{{ order.order_number }} {{ getOrderAge(order.created_at) }} |
{{ order.customer_name }} — | {{ order.order_type?.replace('_', ' ') }} | {{ order.item_count }} items | ${{ parseFloat(order.total).toFixed(2) }} | {{ order.status }} | {{ order.payment_status }} |
|
No orders found
Products
| Product | Category | Price | 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' }} |
|
Categories
Drag to reorder. Order affects display on POS.
{{ category.name }}
{{ category.name_es || '—' }}
Kitchen
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
Ingredients needed at {{ selectedLocation.name }}
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.item_name }} — {{ parseFloat(entry.quantity_produced).toFixed(1) }} {{ entry.unit }} ({{ entry.recipe_name }}, x{{ entry.batch_multiplier }}) {{ entry.linked_product_name }} — {{ Math.abs(parseFloat(entry.quantity_change)).toFixed(1) }} {{ entry.unit }} ({{ entry.waste_reason }})
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 }}
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.title }}
{{ step.description }}
No steps yet. Add steps below.
Add Step
{{ bakingSession.recipe_name }}
Batch: {{ bakingSession.batch_multiplier }}x
{{ bakingSession.steps[bakingSession.current_step - 1].title }}
{{ bakingSession.steps[bakingSession.current_step - 1].description }}
{{ bakingSession.steps[bakingSession.current_step - 1].duration_minutes }} min total
Ingredients ({{ bakingSession.batch_multiplier }}x batch)
{{ (user.role === 'admin' || user.role === 'manager') ? 'My Shift Today' : "Today's Baking" }}
{{ bakingMyShift.status }}No shifts scheduled for this date
{{ shift.user_name }} ({{ shift.user_role }})
| 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.
{{ bakingSelectedShift.user_name }}'s Shift
No items in this shift.
{{ bakingEditingShift ? 'Edit Shift' : 'Create Shift' }}
Shift Items
No recipes found
No items added yet. Search recipes above to add.
Scheduled time exceeds shift duration
Inventory
Products and ingredients across all locations
| 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' : 'Return To Main' }}
{{ transferMode === 'get' ? 'Move stock from Main to ' + (selectedLocation?.name || 'here') + '.' : 'Move stock from ' + (selectedLocation?.name || 'here') + ' back to Main.' }}
{{ row.product_name }}
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) }}
{{ 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 : '' }}.
New shifts go on the open days: {{ scheduleEditableRange.label }}.
Some days in that stretch are locked by a closed pay period and can't take a shift.
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
{{ 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.full_analysis?.summary || aiLatestInsight.summary }}
📊 Sales Analysis
{{ aiLatestInsight.full_analysis?.sales_analysis?.overview }}
Trends
- • {{ trend }}
Peak Hours
🌤️ Weather Impact
{{ aiLatestInsight.full_analysis?.weather_correlation?.impact }}
- • {{ p }}
🍽️ Product Recommendations
Feature These
Consider Promoting
Watch List
✅ Action Items
{{ 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
| 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
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.
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)
Stripe
Accept card payments via Stripe Checkout
{{ stripeConfig.test_secret_key_masked || 'Not set' }}
{{ stripeConfig.test_webhook_secret_masked || 'Not set' }}
{{ stripeConfig.live_secret_key_masked || 'Not set' }}
{{ stripeConfig.live_webhook_secret_masked || 'Not set' }}
Leave blank to auto-detect. Required if the app is behind a proxy.
Webhook URL: {{ stripeConfig.webhook_url || 'Unavailable until app has a public URL' }}
Register this URL in your Stripe dashboard → Webhooks. Events needed: checkout.session.completed
Payment Methods
Cash is always available. Stripe uses a hosted Checkout page and records completed payments back into orders and reports.
🤖 AI Insights
Required for AI insights. Get one at console.anthropic.com
Optional. Enables weather correlation analysis.
Locations
{{ 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.
| Product | On Hand | |
|---|---|---|
|
{{ row.product_name }} {{ row.category_name || '' }} |
{{ row.quantity }} | |
| Loading… No products match "{{ myStockSearch }}" Nothing in stock at this location yet. | ||
{{ user?.nickname || user?.first_name }} {{ user?.last_name }}
{{ user?.email }}
{{ user?.role }}
Currently working at
{{ selectedLocation?.name || 'No location selected' }}
POS Theme
Pick a vibe for your workspace
Dark cards
Flip page-title pills + section labels to dark surface with light text
Your PIN
{{ myPin.pin }}
{{ myPin.reason }}
Your PIN is used to quickly sign in at the terminal.
Time Clock
Clocked in since {{ new Date(timeClockStatus.entry?.clock_in_at).toLocaleTimeString() }}
Not clocked in
| 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 | ||||
{{ myPayView === 'mtd' ? 'Month to Date' : 'Year to Date' }}
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 | ||||
| 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 }}
{{ 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.hours_worked).toFixed(1) }}h
{{ Number(item.regular_hours).toFixed(1) }} reg · {{ Number(item.overtime_hours).toFixed(1) }} OT ×{{ Number(item.ot_multiplier).toFixed(1) }} · {{ Number(item.seventh_day_hours).toFixed(1) }} 7th day
|
${{ 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) }} | {{ item.notes || '' }} | {{ item.status }} | |
| No items. Click Generate to create payroll. | ||||||||
Summary
{{ payrollSelectedPeriod.status === 'processing' ? 'Employee Signature' : 'Manager Signature' }}
Checking this period… Pre-flight checks unavailable {{ payrollPreflightBlocking.length }} thing{{ payrollPreflightBlocking.length === 1 ? '' : 's' }} to fix before you can generate Ready to generate — {{ payrollPreflightWarnings.length }} thing{{ payrollPreflightWarnings.length === 1 ? '' : 's' }} worth a look Everything checks out — ready to generate
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 }}
Manager: {{ payrollSelectedPeriod.verified_by_name }}
Finalized: {{ new Date(payrollSelectedPeriod.finalized_at).toLocaleString() }}
Activity Log
New Pay Period
Creates period for the current week (Monday - Sunday).
Record Payment
{{ payrollPayingItem.nickname || payrollPayingItem.first_name }} — ${{ Number(payrollPayingItem.gross_amount).toFixed(2) }}
Schedule
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
{{ 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 }}
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
Till Count
{{ tillSelectedSession.session_date }} — {{ tillSelectedSession.shift }}
{{ tillSelectedSession.status }}Starting Cash
${{ Number(tillSelectedSession.starting_cash || 0).toFixed(2) }}
Denomination Count
Paid Outs
Receipt Paid Outs
Notes
{{ tillSelectedSession.notes || 'No notes' }}
Summary
{{ tillSelectedSession.status === 'open' ? '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
Manager
Awaiting Manager Verification
Submitted {{ tillSelectedSession.submitted_at ? new Date(tillSelectedSession.submitted_at).toLocaleString() : '' }}
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 }}
Pick a location to see what you can do here.
No apps are enabled for your account at this location.
{{ group.label }}
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 }} |
{{ revealedPins[member.id].pin }}
Not viewable
••••
|
Active Inactive | {{ timeSince(member.last_login) }} |
| 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
{{ s.message }}
{{ s.message }}
{{ s.admin_reply }}