gigafibre-fsm/apps/client/src/pages/CatalogPage.vue
louispaulb a8eeb1b77d refactor: major cleanup — remove dead dispatch app, commit all backend code, extract client composables
- Remove apps/dispatch/ (100% replaced by ops dispatch module, unmaintained)
- Commit services/targo-hub/lib/ (24 modules, 6290 lines — was never tracked)
- Commit services/docuseal + services/legacy-db docker-compose configs
- Extract client app composables: useOTP, useAddressSearch, catalog data, format utils
- Refactor CartPage.vue 630→175 lines, CatalogPage.vue 375→95 lines
- Clean hardcoded credentials from config.js fallback values
- Add client portal: catalog, cart, checkout, OTP verification, address search
- Add ops: NetworkPage, AgentFlowsPage, ConversationPanel, UnifiedCreateModal
- Add ops composables: useBestTech, useConversations, usePermissions, useScanner
- Add field app: scanner composable, docker/nginx configs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 17:38:38 -04:00

110 lines
6.0 KiB
Vue

<template>
<q-page class="catalog-page">
<div class="catalog-hero q-pa-lg text-center text-white">
<div class="text-h4 text-weight-bold q-mb-sm">Nos forfaits et services</div>
<div class="text-subtitle1" style="opacity:.85">Internet fibre ultra-rapide, téléphonie et plus</div>
</div>
<div class="q-px-md q-pt-md" style="max-width:1200px;margin:0 auto">
<q-tabs v-model="activeTab" dense active-color="primary" indicator-color="primary" align="left" narrow-indicator class="text-grey-7">
<q-tab name="all" label="Tous" />
<q-tab name="Internet" label="Internet" />
<q-tab name="Téléphonie" label="Téléphonie" />
<q-tab name="Bundle" label="Bundles" />
<q-tab name="Équipement" label="Équipement" />
</q-tabs>
</div>
<div class="q-pa-md" style="max-width:1200px;margin:0 auto">
<div class="row q-col-gutter-md">
<div v-for="p in filteredProducts" :key="p.item_code" class="col-12 col-sm-6 col-md-4 col-lg-3">
<q-card class="product-card full-height" flat bordered>
<q-badge v-if="p.popular" color="orange" floating label="Populaire" class="text-weight-bold" style="top:12px;right:12px;font-size:.75rem" />
<q-card-section>
<q-chip :color="CATEGORY_COLORS[p.service_category] || 'grey'" text-color="white" size="sm" dense class="q-mb-sm">{{ p.service_category }}</q-chip>
<div class="text-h6 text-weight-bold q-mb-xs">{{ p.item_name }}</div>
<div v-if="p.speed_down" class="speed-bar q-mb-sm">
<div class="row items-center q-gutter-xs">
<q-icon name="speed" color="primary" size="20px" />
<span class="text-weight-medium text-primary">{{ p.speed_down }} / {{ p.speed_up }} Mbps</span>
</div>
<q-linear-progress :value="p.speed_down / 1000" color="primary" class="q-mt-xs" rounded size="6px" />
</div>
<div class="text-body2 text-grey-7 q-mb-md" style="min-height:40px">{{ p.description }}</div>
<div v-if="p.bundle_includes?.length" class="q-mb-md">
<div class="text-caption text-weight-medium text-grey-8 q-mb-xs">Inclus :</div>
<div v-for="inc in p.bundle_includes" :key="inc" class="row items-center q-gutter-xs q-mb-xs">
<q-icon name="check_circle" color="positive" size="16px" />
<span class="text-body2">{{ inc }}</span>
</div>
</div>
<div class="row items-end q-mb-sm">
<span class="text-h5 text-weight-bold text-primary">{{ formatPrice(p.rate) }}</span>
<span class="text-body2 text-grey-6 q-ml-xs q-mb-xs">{{ p.billing_type === 'Mensuel' ? '/mois' : 'unique' }}</span>
</div>
<div v-if="p.requires_visit" class="text-caption text-grey-6 q-mb-sm">
<q-icon name="build" size="14px" class="q-mr-xs" /> Installation requise
</div>
</q-card-section>
<q-separator />
<q-card-actions class="q-pa-md">
<q-btn color="primary" unelevated class="full-width" :label="isInCart(p.item_code) ? 'Ajouté ✓' : 'Ajouter au panier'" :outline="isInCart(p.item_code)" @click="addToCart(p)" />
</q-card-actions>
</q-card>
</div>
</div>
<div v-if="!filteredProducts.length" class="text-center q-pa-xl text-grey-5">
<q-icon name="inventory_2" size="64px" />
<div class="text-h6 q-mt-md">Aucun produit dans cette catégorie</div>
</div>
</div>
<transition name="slide-up">
<div v-if="cart.itemCount > 0" class="cart-bar">
<div class="cart-bar-inner row items-center justify-between q-px-md">
<div class="row items-center q-gutter-sm">
<q-icon name="shopping_cart" size="24px" color="white" />
<span class="text-white text-weight-medium">{{ cart.itemCount }} article{{ cart.itemCount > 1 ? 's' : '' }}</span>
<q-separator vertical dark class="q-mx-sm" />
<span v-if="cart.recurringTotal > 0" class="text-white">{{ formatPrice(cart.recurringTotal) }}/mois</span>
<span v-if="cart.onetimeTotal > 0" class="text-white q-ml-sm">+ {{ formatPrice(cart.onetimeTotal) }} unique</span>
</div>
<q-btn color="white" text-color="primary" unelevated label="Voir le panier" icon-right="arrow_forward" @click="$router.push('/panier')" class="text-weight-bold" />
</div>
</div>
</transition>
</q-page>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useCartStore } from 'src/stores/cart'
import { useQuasar } from 'quasar'
import { CATALOG, CATEGORY_COLORS } from 'src/data/catalog'
import { formatPrice } from 'src/utils/format'
const $q = useQuasar()
const cart = useCartStore()
const activeTab = ref('all')
const filteredProducts = computed(() =>
activeTab.value === 'all' ? CATALOG : CATALOG.filter(p => p.service_category === activeTab.value)
)
const isInCart = (code) => cart.items.some(i => i.item_code === code)
function addToCart (product) {
cart.addItem(product)
$q.notify({ message: `${product.item_name} ajouté au panier`, color: 'positive', icon: 'check_circle', position: 'bottom', timeout: 1500 })
}
</script>
<style lang="scss" scoped>
.catalog-hero { background: linear-gradient(135deg, #3949ab 0%, #1a237e 100%); margin: -16px -16px 0 -16px; padding: 40px 16px 32px }
.product-card { border-radius: 12px; transition: box-shadow 0.2s, transform 0.2s; &:hover { box-shadow: 0 8px 30px rgba(0,0,0,.1); transform: translateY(-2px) } }
.cart-bar { position: fixed; bottom: 0; left: 0; right: 0; z-index: 2000; background: linear-gradient(135deg, #3949ab 0%, #1a237e 100%); box-shadow: 0 -4px 20px rgba(0,0,0,.2) }
.cart-bar-inner { max-width: 1200px; margin: 0 auto; min-height: 56px }
.slide-up-enter-active, .slide-up-leave-active { transition: transform 0.3s ease }
.slide-up-enter-from, .slide-up-leave-to { transform: translateY(100%) }
</style>