Files
BarangaySystem/resources/js/Pages/ManageStoresAdmin.vue
2026-06-06 18:43:00 +08:00

385 lines
14 KiB
Vue

<script setup>
import { ref, onMounted, computed, h } from 'vue'
import axios from 'axios'
import { useNavigate } from '../composables/Core/useNavigate'
import { useModal } from '../composables/Core/useModal'
import { useAuth } from '../composables/Core/useAuth'
import { usePageTitle } from '../composables/Core/usePageTitle'
import LoadingSpinner from '../Components/LoadingSpinner.vue'
import FileImage from '../Components/Core/FileImage.vue'
import BackButton from '../Components/Core/BackButton.vue'
import { useUserSettings } from '../composables/useUserSettings'
import SearchableTableWrapper from '../Components/Core/SearchableTableWrapper.vue'
const { navigate } = useNavigate()
const modal = useModal()
const { isUltimate, isSuperOperator, isOperator, isStoreOwner, user } = useAuth()
const { settings, updateSetting } = useUserSettings();
usePageTitle('Store Management')
const stores = ref([])
const loading = ref(false)
const error = ref(null)
const searchQuery = ref('')
const canModifyStore = (store) => {
if (isStoreOwner.value && !!store.user_can_manage) return true
return !!store.user_can_manage
}
const firstPhoto = (v) => Array.isArray(v) ? (v[0] || '') : (v || '')
const fetchStores = async () => {
loading.value = true
error.value = null
try {
const response = await axios.post('/Admin/Stores/List')
if (response.data && response.data.success && Array.isArray(response.data.stores)) {
stores.value = response.data.stores
} else {
error.value = 'Failed to load stores'
}
} catch (err) {
console.error('Error fetching stores:', err)
error.value = 'Failed to load stores. Please try again.'
} finally {
loading.value = false
}
}
const filteredStores = computed(() => {
if (!searchQuery.value) return stores.value
const q = searchQuery.value.toLowerCase()
return stores.value.filter(s =>
s.name.toLowerCase().includes(q) ||
s.description?.toLowerCase().includes(q) ||
s.category?.toLowerCase().includes(q) ||
s.subcategory?.toLowerCase().includes(q) ||
s.address?.toLowerCase().includes(q)
)
})
const tableDensity = computed({
get: () => settings.value.table_density || 'comfortable',
set: (val) => updateSetting('table_density', val)
});
const toggleStatus = async (store) => {
if (!canModifyStore(store)) {
modal.open({
title: 'Permission Denied',
body: 'You can only modify stores you own or manage.'
})
return
}
try {
const response = await axios.post('/Admin/Store/ToggleStatus', {
target: store.hashkey
})
if (response.data && response.data.success) {
store.is_active = response.data.data.is_active
}
} catch (err) {
console.error('Error toggling store status:', err)
modal.open({
title: 'Error',
body: 'Failed to update status'
})
}
}
const deleteStore = (store) => {
if (!canModifyStore(store)) {
modal.open({
title: 'Permission Denied',
body: 'You can only delete stores you own or manage.'
})
return
}
modal.yesNoModal({
title: 'Confirm Deletion',
body: `Are you sure you want to permanently delete "${store.name}"? This action cannot be undone.`,
yesText: 'Delete',
noText: 'Cancel',
yesClass: 'btn-danger',
onYes: async () => {
try {
const response = await axios.post('/Admin/Store/Delete', {
target: store.hashkey
})
if (response.data && response.data.success) {
stores.value = stores.value.filter(s => s.hashkey !== store.hashkey)
modal.open({
title: 'Success',
body: 'Store deleted successfully.',
footer: h('button', { class: 'btn btn-primary', onClick: modal.close }, 'OK')
})
}
} catch (err) {
console.error('Error deleting store:', err)
modal.open({
title: 'Error',
body: 'Failed to delete store: ' + (err.response?.data?.message || err.message),
footer: h('button', { class: 'btn btn-secondary', onClick: modal.close }, 'Close')
})
}
}
})
}
const editStore = (store) => {
if (!canModifyStore(store)) {
modal.open({
title: 'Permission Denied',
body: 'You can only edit stores you own or manage.'
})
return
}
navigate({ page: 'EditStoreUltimate', props: { target: store.hashkey } })
}
const viewStore = (store) => {
navigate({ page: 'ViewStoreMarket', props: { target: store.hashkey } })
}
const createStore = () => {
navigate({ page: 'CreateStore' })
}
onMounted(() => {
fetchStores()
})
</script>
<template>
<div class="manage-stores-page pb-5">
<div class="tf-container mt-4">
<BackButton to="Home" />
<div class="d-flex align-items-center justify-content-between mb-4">
<div class="d-flex align-items-center gap-3">
<h3 class="fw_6 mb-0">Manage Stores</h3>
<button @click="createStore" class="btn btn-sm btn-primary rounded-pill px-3 py-2 d-flex align-items-center gap-2">
<i class="fas fa-plus"></i> New Store
</button>
<button v-if="isUltimate || isSuperOperator || isOperator" @click="navigate({ page: 'BatchAddStores' })" class="btn btn-sm btn-outline-primary rounded-pill px-3 py-2 d-flex align-items-center gap-2">
<i class="fas fa-file-import"></i> Batch Add
</button>
</div>
<div class="badge bg-soft-primary px-3 py-2 rounded-pill text-primary">
{{ filteredStores.length }} Stores
</div>
</div>
<!-- Searchable Table Wrapper -->
<SearchableTableWrapper
v-model:searchValue="searchQuery"
v-model:densityValue="tableDensity"
:loading="loading"
:error="error"
:empty="filteredStores.length === 0"
searchPlaceholder="Search stores..."
emptyIcon="fas fa-store-slash"
emptyTitle="No stores found"
emptyMessage="Try adjusting your search criteria or create a new store"
:skeletonRows="8"
:skeletonColumns="7"
>
<template #empty-state>
<i class="fas fa-store-slash fa-4x text-muted opacity-25 mb-3"></i>
<h5>No stores found</h5>
<p class="text-muted">Try adjusting your search criteria or create a new store</p>
<button @click="createStore" class="btn btn-primary mt-3">Create First Store</button>
</template>
<template #table>
<thead>
<tr>
<th>Logo</th>
<th>Store Info</th>
<th>Category</th>
<th>Address</th>
<th>Cooperatives</th>
<th>Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="store in filteredStores" :key="store.hashkey">
<td>
<div class="store-thumb">
<FileImage :src="firstPhoto(store.photourl)"
class="img-fluid rounded" alt="Store" fallback="https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/146710fe9ece.bin" />
</div>
</td>
<td>
<div class="fw_6" style="color: var(--text-primary); cursor: pointer;" @click="viewStore(store)">{{ store.name }}</div>
<div class="text-muted small text-truncate" style="max-width: 200px;">{{ store.description }}</div>
</td>
<td>
<div class="small">{{ store.category || 'N/A' }}</div>
<div class="text-muted smallest">{{ store.subcategory || 'N/A' }}</div>
</td>
<td>
<div class="small text-truncate" style="max-width: 150px;">{{ store.address }}</div>
</td>
<td>
<div v-if="store.cooperatives && store.cooperatives.length" class="d-flex flex-wrap gap-1" style="max-width: 200px;">
<span v-for="coop in store.cooperatives" :key="coop.hashkey"
class="badge bg-soft-primary text-primary small">
{{ coop.name }}
</span>
</div>
<span v-else class="text-muted smallest"></span>
</td>
<td>
<div class="form-check form-switch p-0 d-flex justify-content-center">
<input class="form-check-input ms-0" type="checkbox" role="switch"
:checked="store.is_active" @change="toggleStatus(store)"
:disabled="!canModifyStore(store)">
</div>
<div class="text-center smallest mt-1" :class="store.is_active ? 'text-success' : 'text-danger'">
{{ store.is_active ? 'Active' : 'Inactive' }}
</div>
</td>
<td>
<div class="d-flex justify-content-end gap-2">
<button @click="viewStore(store)" class="btn btn-sm btn-icon btn-outline-info" title="View in Market">
<i class="fas fa-eye"></i>
</button>
<button v-if="canModifyStore(store)" @click="navigate({ page: 'AddProductsToStore', props: { target: store.hashkey } })" class="btn btn-sm btn-icon btn-outline-success" title="Assign Products to Store">
<i class="fas fa-boxes"></i>
</button>
<button v-if="canModifyStore(store)" @click="editStore(store)" class="btn btn-sm btn-icon btn-outline-primary" title="Edit">
<i class="fas fa-edit"></i>
</button>
<button v-if="canModifyStore(store)" @click="deleteStore(store)" class="btn btn-sm btn-icon btn-outline-danger" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</template>
</SearchableTableWrapper>
</div>
</div>
</template>
<style scoped>
.manage-stores-page {
background: var(--bg-primary);
min-height: 100vh;
transition: background-color 0.3s ease;
}
.store-table-container {
background: var(--bg-card);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
padding: 1rem;
border: 1px solid var(--border-color);
}
.table {
margin-bottom: 0;
}
.table thead th {
border-top: none;
background: var(--bg-tertiary);
color: var(--accent-color);
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.5px;
padding: 1rem;
}
.table tbody td {
padding: 1rem;
vertical-align: middle;
}
.store-thumb {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: #f0f3f6;
border-radius: 8px;
overflow: hidden;
}
.store-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.badge.bg-soft-primary {
background-color: rgba(66, 185, 131, 0.1);
}
.bg-soft-success {
background-color: rgba(40, 167, 69, 0.1);
}
.bg-soft-danger {
background-color: rgba(220, 53, 69, 0.1);
}
.smallest {
font-size: 0.7rem;
}
.btn-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border-radius: 8px;
}
.form-switch .form-check-input {
width: 2.5em;
height: 1.25em;
cursor: pointer;
}
.form-switch .form-check-input:checked {
background-color: #42b983;
border-color: #42b983;
}
.no-results {
background: var(--bg-card);
border-radius: 20px;
border: 2px dashed var(--border-color);
}
:global(.dark-mode) .manage-stores-page {
background: transparent !important;
}
:global(.dark-mode) .store-table-container {
background: var(--bg-card) !important;
}
:global(.dark-mode) .table thead th {
background: var(--bg-tertiary) !important;
}
:global(.dark-mode) .table tbody td {
color: var(--text-primary) !important;
}
:global(.dark-mode) .no-results {
background: var(--bg-card) !important;
border-color: var(--border-color) !important;
}
</style>