- 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>
110 lines
6.0 KiB
Vue
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>
|