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

341 lines
17 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>