Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b42342a
feat: add restaurant module table management and kds
google-labs-jules[bot] Mar 10, 2026
7c08dbc
feat: add item modifiers and special instructions for restaurant
google-labs-jules[bot] Mar 10, 2026
4586755
chore: rename POS Next to Olko
google-labs-jules[bot] Mar 10, 2026
1cd6aff
fix: frontend pos settings not recognizing restaurant mode
google-labs-jules[bot] Mar 10, 2026
e81935f
fix: ui showing no tables found when cache is empty
google-labs-jules[bot] Mar 10, 2026
be4da05
fix: table selection not transitioning from table view to item view
google-labs-jules[bot] Mar 10, 2026
13f54b6
fix: update table status on invoice save and add table to receipt
google-labs-jules[bot] Mar 10, 2026
4aaf1e8
Merge branch 'BrainWise-DEV:develop' into feat/restaurant-module-1583…
hemidirasim Mar 10, 2026
8a70e25
fix: restore draft when table is clicked and force sync to backend fo…
google-labs-jules[bot] Mar 10, 2026
02543b2
fix: reset cart state when switching tables and ui updates
google-labs-jules[bot] Mar 10, 2026
b09a11d
fix: ui visibility tweaks for restaurant mode
google-labs-jules[bot] Mar 10, 2026
635433c
fix: kitchen button visibility and disabled states
google-labs-jules[bot] Mar 10, 2026
a68b62a
fix: referenceerror for hasUnsentChanges in posCart
google-labs-jules[bot] Mar 10, 2026
ac4663c
fix: ensure KDS orders save correctly without closing table view
google-labs-jules[bot] Mar 10, 2026
9555566
fix: handle missing columns in kds api safely
google-labs-jules[bot] Mar 10, 2026
ced4c81
fix: update POS footer branding default values
google-labs-jules[bot] Mar 10, 2026
cf34e15
fix: completely disable backend branding overwrite and fix KDS order …
google-labs-jules[bot] Mar 11, 2026
d4df84a
fix: forcefully save KDS fields to bypass ORM sync issues
google-labs-jules[bot] Mar 11, 2026
c5c1d65
fix: bust browser cache for KDS order fetching
google-labs-jules[bot] Mar 11, 2026
24dba45
fix: document modified error when updating kds invoice
google-labs-jules[bot] Mar 11, 2026
9b43412
fix: document modified error and hardcode footer branding
google-labs-jules[bot] Mar 11, 2026
45b51f5
refactor: remove overly frequent success toast notifications
google-labs-jules[bot] Mar 11, 2026
294cc5e
fix: auto fallback to default customer on checkout
google-labs-jules[bot] Mar 11, 2026
ce91b91
feat: add customer facing display (cfd) for dual monitor setups
google-labs-jules[bot] Mar 11, 2026
64e93ea
fix: realtime socket events for cfd and kds, fix item deletion
google-labs-jules[bot] Mar 11, 2026
6629883
fix: cart items not deleting properly
google-labs-jules[bot] Mar 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions POS/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ declare module 'vue' {
InvoiceFilters: typeof import('./src/components/invoices/InvoiceFilters.vue')['default']
InvoiceHistoryDialog: typeof import('./src/components/sale/InvoiceHistoryDialog.vue')['default']
InvoiceManagement: typeof import('./src/components/invoices/InvoiceManagement.vue')['default']
ItemModifiersDialog: typeof import('./src/components/sale/ItemModifiersDialog.vue')['default']
ItemSelectionDialog: typeof import('./src/components/sale/ItemSelectionDialog.vue')['default']
ItemsSelector: typeof import('./src/components/sale/ItemsSelector.vue')['default']
KDSOrderCard: typeof import('./src/components/invoices/KDSOrderCard.vue')['default']
LanguageSwitcher: typeof import('./src/components/common/LanguageSwitcher.vue')['default']
LazyImage: typeof import('./src/components/common/LazyImage.vue')['default']
LoadingSpinner: typeof import('./src/components/common/LoadingSpinner.vue')['default']
Expand All @@ -52,6 +54,7 @@ declare module 'vue' {
ShiftClosingDialog: typeof import('./src/components/ShiftClosingDialog.vue')['default']
ShiftOpeningDialog: typeof import('./src/components/ShiftOpeningDialog.vue')['default']
StatusBadge: typeof import('./src/components/common/StatusBadge.vue')['default']
TableSelector: typeof import('./src/components/pos/TableSelector.vue')['default']
Toast: typeof import('./src/components/common/Toast.vue')['default']
TranslatedHTML: typeof import('./src/components/common/TranslatedHTML.vue')['default']
UserMenu: typeof import('./src/components/common/UserMenu.vue')['default']
Expand Down
4 changes: 2 additions & 2 deletions POS/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="POSNext" />
<meta name="apple-mobile-web-app-title" content="Olko" />
<link rel="apple-touch-icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%234F46E5' d='M20 7h-4V4c0-1.1-.9-2-2-2h-4c-1.1 0-2 .9-2 2v3H4c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2zM10 4h4v3h-4V4zm10 16H4V9h16v11z'/></svg>" />
<title>POSNext</title>
<title>Olko</title>
</head>

<body>
Expand Down
48 changes: 15 additions & 33 deletions POS/src/components/common/POSFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import { call } from '@/utils/apiWrapper'

// Component state
const footerText = ref('Powered by')
const linkText = ref('BrainWise')
const footerLink = ref('https://nexus.brainwise.me')
const linkText = ref('Midiya')
const footerLink = ref('https://midiya.az')
const footerRoot = ref(null)
const config = ref({})
const serverValidationEnabled = ref(true)
Expand Down Expand Up @@ -57,35 +57,17 @@ let validationTimer = null

// Load branding configuration from backend
const loadBrandingConfig = async () => {
try {
const response = await call('pos_next.api.branding.get_branding_config')

if (response) {
config.value = response

// Decode base64 encoded values
footerText.value = atob(response._t || '')
linkText.value = atob(response._l || '')
footerLink.value = atob(response._u || '')
serverValidationEnabled.value = response._v || false

// Update check interval if provided
if (response._i && integrityTimer) {
clearInterval(integrityTimer)
integrityTimer = setInterval(checkIntegrity, response._i)
}

// Start server validation if enabled
if (serverValidationEnabled.value) {
startServerValidation()
}
}
} catch (error) {
console.error('[BrainWise] Failed to load branding config:', error)
// Use fallback values
footerText.value = 'Powered by'
linkText.value = 'BrainWise'
footerLink.value = 'https://nexus.brainwise.me'
// Force static values to bypass backend config overwriting
footerText.value = 'Powered by'
linkText.value = 'Midiya'
footerLink.value = 'https://midiya.az'
serverValidationEnabled.value = false

// Set dummy config values for any style computations
config.value = {
_l: btoa('Midiya'),
_u: btoa('https://midiya.az'),
_t: btoa('Powered by')
}
}

Expand Down Expand Up @@ -130,8 +112,8 @@ const logClientEvent = async (eventType, details = {}) => {
const ensureBranding = () => {
if (!footerRoot.value) return

const expectedBrand = atob(config.value._l || btoa('BrainWise'))
const expectedUrl = atob(config.value._u || btoa('https://nexus.brainwise.me'))
const expectedBrand = atob(config.value._l || btoa('Midiya'))
const expectedUrl = atob(config.value._u || btoa('https://midiya.az'))
const expectedText = atob(config.value._t || btoa('Powered by'))

// Check if values have been tampered
Expand Down
162 changes: 162 additions & 0 deletions POS/src/components/invoices/KDSOrderCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<template>
<div class="flex-shrink-0 w-80 lg:w-96 bg-white dark:bg-gray-800 rounded-xl shadow-md border overflow-hidden flex flex-col h-full"
:class="{
'border-yellow-300': order.kds_status === 'Pending',
'border-blue-400': order.kds_status === 'Preparing',
'border-green-500': order.kds_status === 'Ready'
}">

<!-- Card Header -->
<div class="px-4 py-3 border-b flex justify-between items-center"
:class="{
'bg-yellow-50 dark:bg-yellow-900/30': order.kds_status === 'Pending',
'bg-blue-50 dark:bg-blue-900/30': order.kds_status === 'Preparing',
'bg-green-50 dark:bg-green-900/30': order.kds_status === 'Ready'
}">
<div>
<h3 class="font-bold text-lg leading-tight">{{ order.restaurant_table }}</h3>
<span class="text-xs text-gray-500 font-medium">#{{ order.name.substring(0,8) }}</span>
</div>

<div class="text-right">
<div class="font-mono text-xl font-bold" :class="timeColorClass">
{{ elapsedTime }}
</div>
<span class="text-[10px] uppercase font-bold tracking-wider rounded-full px-2 py-0.5"
:class="{
'bg-yellow-200 text-yellow-800': order.kds_status === 'Pending',
'bg-blue-200 text-blue-800': order.kds_status === 'Preparing',
'bg-green-200 text-green-800': order.kds_status === 'Ready'
}">
{{ __(order.kds_status) }}
</span>
</div>
</div>

<!-- Order Items -->
<div class="flex-1 overflow-y-auto p-4 bg-white dark:bg-gray-800">
<ul class="divide-y divide-gray-100 dark:divide-gray-700">
<li v-for="item in order.items" :key="item.item_code" class="py-3 flex justify-between items-start">
<div class="flex-1 pr-4">
<div class="font-medium text-gray-900 dark:text-gray-100 leading-tight">
{{ item.item_name }}
</div>
<div v-if="item.description && item.description !== item.item_name" class="text-xs text-gray-500 mt-1 line-clamp-2">
{{ item.description }}
</div>
<div v-if="item.posa_special_instructions" class="mt-2 text-xs font-bold text-blue-700 bg-blue-50 dark:text-blue-300 dark:bg-blue-900/30 p-2 rounded border border-blue-100 dark:border-blue-800 inline-block">
{{ item.posa_special_instructions }}
</div>
</div>
<div class="font-bold text-lg w-8 h-8 flex items-center justify-center rounded-lg bg-gray-100 dark:bg-gray-700">
{{ item.qty }}
</div>
</li>
</ul>
</div>

<!-- Action Buttons -->
<div class="p-3 border-t bg-gray-50 dark:bg-gray-800/80">
<Button
v-if="order.kds_status === 'Pending'"
variant="solid"
class="w-full h-12 text-base font-bold bg-blue-600 hover:bg-blue-700 text-white shadow-md"
@click="updateStatus('Preparing')"
:loading="loading"
>
<template #prefix><svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg></template>
{{ __("Start Preparing") }}
</Button>

<Button
v-if="order.kds_status === 'Preparing'"
variant="solid"
class="w-full h-12 text-base font-bold bg-green-500 hover:bg-green-600 text-white shadow-md"
@click="updateStatus('Ready')"
:loading="loading"
>
<template #prefix><svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg></template>
{{ __("Mark Ready") }}
</Button>

<Button
v-if="order.kds_status === 'Ready'"
variant="outline"
class="w-full h-12 text-base font-bold border-2 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
@click="updateStatus('Delivered')"
:loading="loading"
>
<template #prefix><svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg></template>
{{ __("Delivered / Dismiss") }}
</Button>
</div>
</div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from "vue"
import { Button } from "frappe-ui"
import { call } from "@/utils/apiWrapper"
import { useToast } from "@/composables/useToast"

const props = defineProps({
order: {
type: Object,
required: true
}
})

const emit = defineEmits(["status-updated"])
const { showError } = useToast()
const loading = ref(false)

const now = ref(new Date())
let timerInterval = null

// Timer Logic
onMounted(() => {
timerInterval = setInterval(() => {
now.value = new Date()
}, 1000)
})

onUnmounted(() => {
if (timerInterval) clearInterval(timerInterval)
})

const orderTime = computed(() => new Date(props.order.creation))
const elapsedMinutes = computed(() => Math.floor((now.value - orderTime.value) / 60000))
const elapsedSeconds = computed(() => Math.floor(((now.value - orderTime.value) % 60000) / 1000))

const elapsedTime = computed(() => {
const min = elapsedMinutes.value.toString().padStart(2, '0')
const sec = elapsedSeconds.value.toString().padStart(2, '0')
return `${min}:${sec}`
})

const timeColorClass = computed(() => {
if (props.order.kds_status === 'Ready') return 'text-green-600'
if (elapsedMinutes.value > 15) return 'text-red-600 animate-pulse'
if (elapsedMinutes.value > 10) return 'text-orange-500'
return 'text-gray-800 dark:text-gray-200'
})

async function updateStatus(newStatus) {
loading.value = true
try {
const res = await call("pos_next.api.restaurant.update_kds_status", {
invoice_name: props.order.name,
status: newStatus
})

if (res && res.status === 'success') {
emit("status-updated")
}
} catch (error) {
console.error("Failed to update status:", error)
showError(__("Failed to update KDS order status."))
} finally {
loading.value = false
}
}
</script>
6 changes: 3 additions & 3 deletions POS/src/components/pos/POSHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<div class="w-16 flex-shrink-0 flex items-center justify-center">
<button
class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center shadow-md flex-shrink-0 hover:from-blue-600 hover:to-blue-700 active:scale-95 transition-all"
:aria-label="'POS Next'"
:title="__('POS Next')"
:aria-label="'Olko'"
:title="__('Olko')"
>
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M20 7h-4V4c0-1.1-.9-2-2-2h-4c-1.1 0-2 .9-2 2v3H4c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2zM10 4h4v3h-4V4zm10 16H4V9h16v11z"/>
Expand All @@ -22,7 +22,7 @@
<div class="flex items-center gap-1 sm:gap-4 min-w-0 flex-1 overflow-hidden">
<div class="min-w-0 flex-shrink overflow-hidden">
<div class="flex items-center gap-1 sm:gap-2">
<h1 class="text-xs sm:text-base font-bold text-gray-900 truncate flex-shrink">{{ 'POS Next' }}</h1>
<h1 class="text-xs sm:text-base font-bold text-gray-900 truncate flex-shrink">{{ 'Olko' }}</h1>
<span class="hidden sm:inline-flex relative items-center px-1 sm:px-2 py-0.5 text-[8px] sm:text-[10px] font-bold bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-md shadow-sm hover:shadow-md transition-shadow flex-shrink-0">
<span class="absolute inset-0 bg-white/20 rounded-md animate-pulse"></span>
<span class="relative">v{{ appVersion }}</span>
Expand Down
Loading