Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
107 changes: 87 additions & 20 deletions POS/src/components/sale/ItemsSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,31 @@
<div class="px-1.5 sm:px-3 pt-1.5 sm:pt-3 pb-1.5 sm:pb-2 bg-white border-b border-gray-200">
<div class="flex items-center gap-1 sm:gap-2 overflow-x-auto pb-1 scrollbar-hide snap-x snap-mandatory">
<button
@click="itemStore.setSelectedItemGroup(null)"
@click="handleAllFilterClick"
:class="[
'flex items-center px-2 sm:px-3 py-1.5 sm:py-2 rounded-lg text-[10px] sm:text-xs font-medium whitespace-nowrap transition-[background-color,border-color] duration-75 touch-manipulation snap-start flex-shrink-0',
!selectedItemGroup
!activeFilterValue
? 'bg-blue-50 text-blue-600 border-2 border-blue-500 shadow-sm'
: 'bg-white text-gray-700 border border-gray-200 hover:bg-gray-50 active:bg-gray-100',
]"
>
<svg class="w-3.5 h-3.5 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
<span>{{ __('All Items') }}</span>
<span>{{ isBrandSortActive ? __('All Brands') : __('All Items') }}</span>
</button>
<button
v-for="group in itemGroups"
:key="group.item_group"
@click="itemStore.setSelectedItemGroup(group.item_group)"
v-for="option in activeFilterOptions"
:key="option.value"
@click="handleFilterClick(option.value)"
:class="[
'flex items-center px-2 sm:px-3 py-1.5 sm:py-2 rounded-lg text-[10px] sm:text-xs font-medium whitespace-nowrap transition-[background-color,border-color] duration-75 touch-manipulation snap-start flex-shrink-0',
selectedItemGroup === group.item_group
activeFilterValue === option.value
? 'bg-blue-50 text-blue-600 border-2 border-blue-500 shadow-sm'
: 'bg-white text-gray-700 border border-gray-200 hover:bg-gray-50 active:bg-gray-100',
]"
>
<span>{{ __(group.item_group) }}</span>
<span>{{ __(option.label) }}</span>
</button>
</div>
</div>
Expand Down Expand Up @@ -201,7 +201,7 @@

<!-- Sort Options Loop -->
<button
v-for="option in SORT_OPTIONS"
v-for="option in sortOptions"
:key="option.field"
@click="handleSortToggle(option.field)"
:class="[
Expand Down Expand Up @@ -260,9 +260,9 @@
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
/>
</svg>
<p v-if="searchTerm || selectedItemGroup" class="mt-2 text-xs font-medium text-gray-700">
<span v-if="searchTerm && selectedItemGroup">{{ __('No results for {0} in {1}', [searchTerm, selectedItemGroup]) }}</span>
<span v-else-if="selectedItemGroup">{{ __('No results in {0}', [selectedItemGroup]) }}</span>
<p v-if="searchTerm || selectedFilterLabel" class="mt-2 text-xs font-medium text-gray-700">
<span v-if="searchTerm && selectedFilterLabel">{{ __('No results for {0} in {1}', [searchTerm, selectedFilterLabel]) }}</span>
<span v-else-if="selectedFilterLabel">{{ __('No results in {0}', [selectedFilterLabel]) }}</span>
<span v-else>{{ __('No results for {0}', [searchTerm]) }}</span>
</p>
<p v-else class="mt-2 text-xs text-gray-500">{{ __('No items available') }}</p>
Expand Down Expand Up @@ -762,7 +762,9 @@ const {
filteredItems,
searchTerm,
selectedItemGroup,
selectedBrand,
itemGroups,
brands,
loading,
loadingMore,
hasMore,
Expand Down Expand Up @@ -843,7 +845,7 @@ const SEARCH_PLACEHOLDERS = Object.freeze({
})

// Sort configuration
const SORT_OPTIONS = Object.freeze([
const BASE_SORT_OPTIONS = Object.freeze([
{
field: 'name',
label: __('Name'),
Expand All @@ -854,11 +856,6 @@ const SORT_OPTIONS = Object.freeze([
label: __('Quantity'),
icon: 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4'
},
{
field: 'item_group',
label: __('Item Group'),
icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10'
},
{
field: 'price',
label: __('Price'),
Expand All @@ -871,6 +868,19 @@ const SORT_OPTIONS = Object.freeze([
}
])

const CONTEXT_SORT_OPTIONS = Object.freeze({
brand: {
field: 'brand',
label: __('Brand'),
icon: 'M20 13V7a2 2 0 00-2-2h-4V3H10v2H6a2 2 0 00-2 2v6M8 21h8a2 2 0 002-2v-5H6v5a2 2 0 002 2z'
},
item_group: {
field: 'item_group',
label: __('Item Group'),
icon: 'M9 12l2 2 4-4m5.586 1.414l-6.172 6.172a2 2 0 01-2.828 0L3.414 9.414a2 2 0 010-2.828l6.172-6.172a2 2 0 012.828 0l8.172 8.172a2 2 0 010 2.828z'
},
})

const SORT_ICONS = Object.freeze({
ascending: 'M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12',
descending: 'M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4',
Expand All @@ -890,6 +900,32 @@ const searchMode = computed(() => {
})

const searchPlaceholder = computed(() => SEARCH_PLACEHOLDERS[searchMode.value])
const isBrandSortActive = computed(() => sortBy.value === 'brand')
const sortOptions = computed(() => {
// Context switcher:
// - In Item Group mode, offer Brand.
// - In Brand mode, offer Item Group.
const contextSort = isBrandSortActive.value
? CONTEXT_SORT_OPTIONS.item_group
: CONTEXT_SORT_OPTIONS.brand

return [
BASE_SORT_OPTIONS[0],
contextSort,
BASE_SORT_OPTIONS[1],
BASE_SORT_OPTIONS[2],
BASE_SORT_OPTIONS[3],
]
})
const activeFilterValue = computed(() => (
isBrandSortActive.value ? selectedBrand.value : selectedItemGroup.value
))
const activeFilterOptions = computed(() => (
isBrandSortActive.value
? (brands.value || []).map((b) => ({ value: b.brand, label: b.brand }))
: (itemGroups.value || []).map((g) => ({ value: g.item_group, label: g.item_group }))
))
const selectedFilterLabel = computed(() => selectedBrand.value || selectedItemGroup.value || null)

// Watch for cart items and pos profile changes (optimized - uses length + hash instead of deep watch)
// Tracks: length, item_code, quantity, and amount to detect all cart changes including array replacements
Expand Down Expand Up @@ -1164,6 +1200,22 @@ function setViewMode(mode) {
userManuallySetView.value = true
}

function handleAllFilterClick() {
if (isBrandSortActive.value) {
itemStore.setSelectedBrand(null)
return
}
itemStore.setSelectedItemGroup(null)
}

function handleFilterClick(value) {
if (isBrandSortActive.value) {
itemStore.setSelectedBrand(value)
return
}
itemStore.setSelectedItemGroup(value)
}

// Pagination functions — each page fetches fresh data from server
function goToPage(page) {
if (page >= 1 && page <= totalPages.value && page !== currentPage.value) {
Expand Down Expand Up @@ -1243,9 +1295,24 @@ function handleSortToggle(field) {
}
}

watch(sortBy, async (newSortBy, oldSortBy) => {
if (newSortBy === 'brand') {
await itemStore.loadBrands()
if (selectedItemGroup.value) {
await itemStore.setSelectedItemGroup(null)
}
return
}

if (oldSortBy === 'brand' && selectedBrand.value) {
await itemStore.setSelectedBrand(null)
}
})

function getSortLabel(sortByValue) {
const option = SORT_OPTIONS.find(opt => opt.field === sortByValue)
return option?.label || sortByValue
return CONTEXT_SORT_OPTIONS[sortByValue]?.label
|| BASE_SORT_OPTIONS.find(opt => opt.field === sortByValue)?.label
|| sortByValue
}

function getSortIconState(field) {
Expand Down
Loading
Loading