initial: bootstrap from BukidBountyApp base
This commit is contained in:
341
resources/js/Pages/Fragments/Home/HomeUltimate.vue
Normal file
341
resources/js/Pages/Fragments/Home/HomeUltimate.vue
Normal file
@@ -0,0 +1,341 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, h, computed } from 'vue';
|
||||
import usePageData from '../../../composables/usePageData.js';
|
||||
import { useNavigate } from '../../../composables/Core/useNavigate.js';
|
||||
import { useUserNotes } from '../../../composables/useUserNotes.js';
|
||||
import { useModal } from '../../../composables/Core/useModal.js';
|
||||
import { useAuth } from '../../../composables/Core/useAuth.js';
|
||||
import { useGlobalTransactions } from '../../../composables/useGlobalTransactions.js';
|
||||
import { useActivity } from '../../../composables/useActivity.js';
|
||||
import { usePrefetch } from '../../../composables/Core/usePrefetch.js';
|
||||
import { useUIStore } from '../../../stores/ui.js';
|
||||
|
||||
// Core Components
|
||||
import BalanceBox from '../../../Components/Core/Stats/BalanceBox.vue';
|
||||
import ServiceButtonGrid from '../../../Components/Core/Services/ServiceButtonGrid.vue';
|
||||
import SideTextButtonList from '../../../Components/Core/Services/SideTextButtonList.vue';
|
||||
import SearchableList from '../../../Components/Core/Search/SearchableList.vue';
|
||||
import CardSimple from '../../../Components/Core/CardSimple.vue';
|
||||
import GlobalAnnouncement from '../../../Components/GlobalAnnouncement.vue';
|
||||
import HomeSkeleton from '../../../Components/Core/Skeleton/HomeSkeleton.vue';
|
||||
import CooperativeDetail from '@/Pages/CooperativeDetail.vue';
|
||||
import DocumentRepository from '@/Pages/Fragments/DocumentRepository.vue';
|
||||
import GovernanceResolutions from '@/Pages/Fragments/GovernanceResolutions.vue';
|
||||
|
||||
const { hasRole, UserTypes, user } = useAuth();
|
||||
const { precache } = useGlobalTransactions();
|
||||
const url = '/home-data';
|
||||
const payload = {};
|
||||
|
||||
const { data, loading, error, stale, fetchPageData } = usePageData();
|
||||
const { navigate } = useNavigate();
|
||||
const { notes, fetchNotes, dismissNotes, hasNotes } = useUserNotes();
|
||||
const { activities, fetchRecentActivities, loading: loadingActivities } = useActivity();
|
||||
const { prefetchEverything } = usePrefetch();
|
||||
const modal = useModal();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const showStaleIndicator = ref(false);
|
||||
|
||||
// Stats data mapping to enhanced backend
|
||||
const stats = ref([
|
||||
{ title: "Pending", number: 0, unit: "Orders", align: "left", numberId: "pending_orders_no" },
|
||||
{ title: "Stores", number: 0, unit: "Active", align: "left", numberId: "active_stores_no" },
|
||||
{ title: "Users", number: 0, unit: "Total", align: "right", numberId: "total_users_no" },
|
||||
{ title: "Balance", number: 0, unit: "PHP", align: "right", numberId: "total_balance_php" }
|
||||
]);
|
||||
|
||||
const balanceFooterItems = ref([
|
||||
{ title: "Add Transaction", icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/c9fd442fe676.bin", pagename: "AddTransaction" },
|
||||
{ title: "View Reports", icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/f87407046b18.bin", pagename: "ListReports" }
|
||||
]);
|
||||
|
||||
const activeOrgHash = computed(() => {
|
||||
const coops = user.value?.settings?.cooperatives;
|
||||
if (Array.isArray(coops) && coops.length > 0) {
|
||||
return coops[0];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const hubTab = ref('overview');
|
||||
|
||||
// Helper to filter items based on roles and module state
|
||||
const filterByRole = (items) => {
|
||||
return items.filter(item => {
|
||||
if (item.module && !uiStore.isModuleEnabled(item.module)) return false;
|
||||
if (!item.roles || item.roles === 'all') return true;
|
||||
if (Array.isArray(item.roles)) {
|
||||
return item.roles.some(role => hasRole(role));
|
||||
}
|
||||
return hasRole(item.roles);
|
||||
});
|
||||
};
|
||||
|
||||
// Primary Grid Services
|
||||
const services = computed(() => filterByRole([
|
||||
{ icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/32248fe10b94.bin', title: 'Users', pagename: 'UserList', roles: [UserTypes.ULTIMATE, UserTypes.SUPER_OPERATOR] },
|
||||
{ icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/85605eacd4c8.bin', title: 'Stores', pagename: 'ManageStoresAdmin', roles: [UserTypes.ULTIMATE, UserTypes.SUPER_OPERATOR, UserTypes.OPERATOR] },
|
||||
{ icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/ef1a9a079a2d.svg', title: 'Products', pagename: 'ManageProductsAdmin', roles: [UserTypes.ULTIMATE, UserTypes.SUPER_OPERATOR] },
|
||||
{ icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/c9fd442fe676.bin', title: 'Transactions', pagename: 'ManageGlobalTransactions', roles: [UserTypes.ULTIMATE, UserTypes.SUPER_OPERATOR] },
|
||||
{ icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/f87407046b18.bin', title: 'Reports', pagename: 'ListReports', roles: [UserTypes.ULTIMATE, UserTypes.SUPER_OPERATOR] },
|
||||
{ icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/fa711c34b4ef.svg', title: 'Accounting', pagename: 'AccountingDashboard', roles: [UserTypes.ULTIMATE, UserTypes.SUPER_OPERATOR, UserTypes.OPERATOR], module: 'accounting' },
|
||||
{ icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/5b5ef88c0ad1.svg', title: 'POS Keys', pagename: 'PosAccessKeys', roles: [UserTypes.ULTIMATE, UserTypes.SUPER_OPERATOR, UserTypes.OPERATOR] },
|
||||
{ icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/04d0e432a298.bin', title: 'Market', pagename: 'ListProductsMarket', roles: 'all' },
|
||||
{ icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/4d9cb130fad1.bin', title: 'Shipments', pagename: 'ShipmentList', roles: 'all' },
|
||||
{ icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/5d0ad5d52b8c.bin', title: 'Farmers', pagename: 'FarmerProfileEdit', roles: 'all' },
|
||||
{ icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/3360da347e6b.svg', title: 'Verification', pagename: 'VerificationDashboard', roles: [UserTypes.ULTIMATE, UserTypes.SUPER_OPERATOR, UserTypes.OPERATOR] },
|
||||
{ icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/d9b9b9179ce0.svg', title: 'Cooperatives', pagename: 'CooperativeList', roles: 'all' },
|
||||
{ icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/01f266928e54.svg', title: 'Console', pagename: 'UltimateConsole', roles: [UserTypes.ULTIMATE] },
|
||||
{ icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/3d721f4acf47.svg', title: 'Announce', pagename: 'ManageAnnouncements', roles: [UserTypes.ULTIMATE] },
|
||||
]));
|
||||
|
||||
// Secondary List Actions
|
||||
const quickActionsItems = computed(() => filterByRole([
|
||||
{ text: 'Onboard New User', pagename: 'CreateUser', icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/516ed2aaaa4c.bin', roles: [UserTypes.ULTIMATE, UserTypes.SUPER_OPERATOR] },
|
||||
{ text: 'Register New Store', pagename: 'CreateStore', icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/85605eacd4c8.bin', roles: [UserTypes.ULTIMATE, UserTypes.SUPER_OPERATOR, UserTypes.OPERATOR] },
|
||||
{ text: 'Create New Product', pagename: 'CreateProductUltimate', icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/f0a0193d728e.bin', roles: [UserTypes.ULTIMATE, UserTypes.SUPER_OPERATOR] },
|
||||
{ text: 'Create New Cooperative', pagename: 'CreateCooperative', icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/d9b9b9179ce0.svg', roles: [UserTypes.ULTIMATE, UserTypes.SUPER_OPERATOR] },
|
||||
{ text: 'Add Organization', pagename: 'CreateOrganization', icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/f0cc8da0402c.svg', roles: [UserTypes.ULTIMATE, UserTypes.SUPER_OPERATOR, UserTypes.OPERATOR] },
|
||||
{ text: 'My Personal Profile', pagename: 'UserInfoEdit', pagestring: user.hashkey, icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/ac7a1cebe580.bin', roles: 'all' },
|
||||
{ text: 'Referrals & Leads', pagename: 'ListReferrals', icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/7d45d4bdbc74.bin', roles: 'all', module: 'properties' },
|
||||
{ text: 'Property Listings', pagename: 'ListProperties', icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/53c45417d1d1.bin', roles: 'all', module: 'properties' },
|
||||
{ text: 'Send Credit Transfer', pagename: 'TransferMyCredit', icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/074af7aca12b.bin', roles: 'all' },
|
||||
{ text: 'Global System Settings', pagename: 'SystemSettings', icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/b1e4fd15d8bd.bin', roles: [UserTypes.ULTIMATE] },
|
||||
{ text: 'Landing Page Editor', pagename: 'LandingPageEditor', icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/3d721f4acf47.svg', roles: [UserTypes.ULTIMATE, UserTypes.SUPER_OPERATOR, UserTypes.COORDINATOR] },
|
||||
]));
|
||||
|
||||
// Remove internal reactive recentItems as we use useActivity now
|
||||
|
||||
const showNotesModal = () => {
|
||||
modal.continueCancelModal({
|
||||
title: 'Notes',
|
||||
body: h('div', {
|
||||
style: 'white-space: pre-wrap; font-size: 16px; line-height: 1.5; color: #333;'
|
||||
}, notes.value),
|
||||
continueText: 'Dismiss Note',
|
||||
cancelText: 'Close',
|
||||
continueClass: 'btn btn-danger w-50 py-2 rounded-3 shadow-sm fw-bold',
|
||||
cancelClass: 'btn btn-light w-50 py-2 rounded-3 border fw-bold text-muted',
|
||||
onContinue: async () => {
|
||||
const success = await dismissNotes();
|
||||
if (success) {
|
||||
await fetchNotes();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
precache(); // Precache transactions for smoother experience
|
||||
fetchRecentActivities(10); // Fetch real activities
|
||||
const result = await fetchPageData(url, payload);
|
||||
|
||||
if (result && result.stale) {
|
||||
showStaleIndicator.value = true;
|
||||
}
|
||||
|
||||
// Update stats if data provides them
|
||||
if (data.value?.stats) {
|
||||
stats.value = stats.value.map(s => {
|
||||
if (data.value.stats[s.numberId] !== undefined) {
|
||||
return { ...s, number: data.value.stats[s.numberId] };
|
||||
}
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch notes and auto-show if they exist
|
||||
await fetchNotes();
|
||||
if (hasNotes()) {
|
||||
showNotesModal();
|
||||
}
|
||||
|
||||
// --- Start background universal prefetch ---
|
||||
// User wants "everything" preloaded once they land on home.
|
||||
// Staggered trigger to ensure smooth initial experience.
|
||||
setTimeout(() => {
|
||||
prefetchEverything();
|
||||
}, 1000); // 1-second delay to give priority to dashboard data
|
||||
});
|
||||
|
||||
const handleItemClick = (item) => {
|
||||
if (item.pagename) {
|
||||
navigate({ page: item.pagename, props: { data: item.pagestring || '' } });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="home-ultimate-fragment pb-5">
|
||||
<!-- Global loading skeleton -->
|
||||
<HomeSkeleton v-if="loading && !data" />
|
||||
|
||||
<!-- Stale data notification -->
|
||||
<div v-if="showStaleIndicator" class="stale-notice">
|
||||
⚠️ Displaying cached command center data.
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div v-if="data">
|
||||
<!-- Balance / Stats Box -->
|
||||
<BalanceBox :stats="stats" :footer-items="balanceFooterItems" @footer-click="handleItemClick" />
|
||||
|
||||
<!-- Primary Services Grid -->
|
||||
<div class="mt-2">
|
||||
<ServiceButtonGrid :items="services" @item-click="handleItemClick" />
|
||||
</div>
|
||||
|
||||
<!-- Global Announcements -->
|
||||
<div class="mt-3">
|
||||
<GlobalAnnouncement />
|
||||
</div>
|
||||
|
||||
<!-- Primary Cooperative Hub -->
|
||||
<div v-if="activeOrgHash" class="mt-4 px-3">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h5 class="fw_7 mb-0 d-flex align-items-center gap-2" style="color: var(--text-primary) !important;">
|
||||
<i class="fas fa-landmark text-primary opacity-50"></i>
|
||||
Cooperative Hub
|
||||
</h5>
|
||||
<div class="d-flex gap-1 bg-soft-primary p-1 rounded-pill">
|
||||
<button
|
||||
@click="hubTab = 'overview'"
|
||||
:class="['btn btn-xs rounded-pill px-3 transition-all', hubTab === 'overview' ? 'btn-primary shadow-sm' : 'btn-transparent text-primary small']"
|
||||
>
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
@click="hubTab = 'docs'"
|
||||
:class="['btn btn-xs rounded-pill px-3 transition-all', hubTab === 'docs' ? 'btn-primary shadow-sm' : 'btn-transparent text-primary small']"
|
||||
>
|
||||
Docs
|
||||
</button>
|
||||
<button
|
||||
@click="hubTab = 'votes'"
|
||||
:class="['btn btn-xs rounded-pill px-3 transition-all', hubTab === 'votes' ? 'btn-primary shadow-sm' : 'btn-transparent text-primary small']"
|
||||
>
|
||||
Resolutions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm rounded-20 bg-white overflow-hidden p-0">
|
||||
<div v-if="hubTab === 'overview'">
|
||||
<CooperativeDetail :target="activeOrgHash" />
|
||||
</div>
|
||||
<div v-else-if="hubTab === 'docs'" class="p-3">
|
||||
<DocumentRepository :org-hash="activeOrgHash" />
|
||||
</div>
|
||||
<div v-else-if="hubTab === 'votes'" class="p-3">
|
||||
<GovernanceResolutions :org-hash="activeOrgHash" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detailed Quick Actions -->
|
||||
<div class="tf-container mt-4">
|
||||
<h5 class="fw_7 mb-3" style="color: var(--text-primary) !important;">Management & Tools</h5>
|
||||
<SideTextButtonList :items="quickActionsItems" @item-click="handleItemClick" />
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity List -->
|
||||
<div class="mt-4 px-3">
|
||||
<div class="activity-section card border-0 shadow-sm rounded-4 bg-white overflow-hidden">
|
||||
<div class="card-header bg-white border-0 py-3 px-4 d-flex align-items-center justify-content-between">
|
||||
<h5 class="fw-bold mb-0 d-flex align-items-center gap-2">
|
||||
<i class="fas fa-history text-primary"></i>
|
||||
Recent System Activity
|
||||
</h5>
|
||||
<a
|
||||
href="javascript:void(0);"
|
||||
class="btn btn-sm btn-outline-primary rounded-pill px-3 fw-semibold"
|
||||
@click="navigate({ page: 'ManageGlobalTransactions' })"
|
||||
>
|
||||
View All
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<SearchableList
|
||||
title=""
|
||||
:items="activities"
|
||||
:loading="loadingActivities"
|
||||
empty-text="No recent activity recorded"
|
||||
@item-click="handleItemClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error display -->
|
||||
<div v-if="error && !data">
|
||||
<div class="tf-container mt-5 text-center">
|
||||
<p style="color: var(--text-muted);">Command center disconnected. Please retry.</p>
|
||||
<button class="btn btn-primary mt-2 rounded-pill px-4" @click="fetchPageData(url, payload)">Reconnect</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.spinner-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 5px solid var(--bg-tertiary);
|
||||
border-top-color: var(--accent-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.stale-notice {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.announcement-img {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.announcement-text {
|
||||
line-height: 1.6;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 50rem;
|
||||
}
|
||||
|
||||
.bg-soft-primary {
|
||||
background-color: rgba(var(--primary-rgb), 0.1);
|
||||
}
|
||||
|
||||
.transition-all {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user