+
+
Administration Catalogues
+
Gérer le scraping des catalogues Bonial
+
+
+ {/* Stats Cards */}
+ {stats && (
+
+
+
Total Catalogues
+
{stats.total_catalogues}
+
+
+
Dernière Mise à Jour
+
+ {stats.derniere_mise_a_jour
+ ? formatDate(stats.derniere_mise_a_jour)
+ : 'Jamais'}
+
+
+
+
Prochaine Exécution
+
{stats.prochaine_execution}
+
+
+ )}
+
+ {/* Manual Scraping */}
+
+
Scraping Manuel
+
+
+
+
+
Ou scraper une enseigne spécifique:
+
+ {enseignes.map(enseigne => (
+
+ ))}
+
+
+
+
+
+ {/* Catalogues par Enseigne */}
+ {stats && (
+
+
Catalogues par Enseigne
+
+ {Object.entries(stats.catalogues_par_enseigne).map(([nom, count]) => (
+
+ {nom}
+ {count}
+
+ ))}
+
+
+ )}
+
+ {/* Scraping Logs */}
+
+
+
Historique des Scraping
+
+
+
+ {loading ? (
+
+
+
+ ) : logs.length === 0 ? (
+
Aucun log disponible
+ ) : (
+
+ {logs.map(log => (
+
+
+ {getStatusIcon(log.statut)}
+
+
+
+ {log.enseigne_nom || 'Toutes'}
+
+ {formatDate(log.date_execution)}
+
+
+
+ {log.catalogues_trouves} trouvé(s) • {log.catalogues_nouveaux} nouveau(x)
+ {log.duree_secondes && ` • ${log.duree_secondes.toFixed(1)}s`}
+
+ {log.message_erreur && (
+
+ {log.message_erreur}
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/layout/Layout.jsx b/frontend/src/components/layout/Layout.jsx
index 46c4dbe..04b038e 100644
--- a/frontend/src/components/layout/Layout.jsx
+++ b/frontend/src/components/layout/Layout.jsx
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { LayoutDashboard, Search, TrendingUp, Moon, Sun, Menu, LogOut, Shield } from 'lucide-react';
+import { LayoutDashboard, Search, TrendingUp, BookOpen, Moon, Sun, Menu, LogOut, Shield } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { cn } from '@/lib/utils';
@@ -23,6 +23,7 @@ const Layout = ({ children, theme, toggleTheme }) => {
{ icon: LayoutDashboard, label: t('nav.dashboard'), path: '/' },
{ icon: Search, label: t('nav.search') || 'Recherche', path: '/search' },
{ icon: TrendingUp, label: 'Comparateur', path: '/compare' },
+ { icon: BookOpen, label: 'Catalogues', path: '/catalogues' },
];
// Add admin link for admin users
diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx
index 1d7af6f..0971f0e 100644
--- a/frontend/src/pages/Admin.jsx
+++ b/frontend/src/pages/Admin.jsx
@@ -7,7 +7,7 @@ import {
Settings as SettingsIcon, Cpu, Clock, Bell, Globe,
ChevronDown, ChevronUp, RefreshCw, RotateCcw, Edit2,
Sparkles, Eye, Code, Brain, DollarSign, Filter,
- TrendingDown, CheckCircle2, Package, Bug, Download
+ TrendingDown, CheckCircle2, Package, Bug, Download, BookOpen
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
@@ -18,6 +18,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Slider } from '@/components/ui/slider';
import { cn } from '@/lib/utils';
import { useAuth } from '@/hooks/use-auth';
+import CatalogueAdmin from '@/components/dashboard/CatalogueAdmin';
const API_URL = '/api';
@@ -495,6 +496,7 @@ export default function Admin() {
{ id: 'sites', label: 'Sites', icon: Globe },
{ id: 'jobs', label: 'Jobs', icon: Clock },
{ id: 'notifications', label: 'Notifications', icon: Bell },
+ { id: 'catalogues', label: 'Catalogues', icon: BookOpen },
{ id: 'debug', label: 'Debug', icon: Bug },
];
@@ -1120,6 +1122,9 @@ export default function Admin() {
)}
-
+
+ {/* Catalogues Tab */}
+ {activeTab === 'catalogues' &&
+ {/* Header */}
+
+
Catalogues Promotionnels
+
+ Consultez les catalogues et prospectus des enseignes discount
+
+
+
+ {/* Search Bar */}
+
+
+
+ {
+ setSearchQuery(e.target.value);
+ setPagination(prev => ({ ...prev, page: 1 }));
+ }}
+ />
+
+
+
+ {/* Enseignes Filter */}
+
+
+
+ {enseignes.map(enseigne => (
+
+ ))}
+
+
+
+ {/* Catalogues Grid */}
+ {loading ? (
+
+
+
+ ) : catalogues.length === 0 ? (
+
+
+
Aucun catalogue trouvé
+
+ {searchQuery
+ ? 'Aucun catalogue ne correspond à votre recherche'
+ : 'Aucun catalogue disponible pour le moment'}
+
+
+ ) : (
+ <>
+
+ {catalogues.map(catalogue => (
+
openCatalogue(catalogue)}
+ >
+ {/* Cover Image */}
+
+

+
+ {isValidNow(catalogue) && (
+
+
+ En cours
+
+
+ )}
+
+
+ {/* Content */}
+
+ {/* Enseigne Badge */}
+
+ {catalogue.enseigne.nom}
+
+
+ {/* Title */}
+
+ {catalogue.titre}
+
+
+ {/* Dates */}
+
+
+
+ {formatDate(catalogue.date_debut)} - {formatDate(catalogue.date_fin)}
+
+
+
+ {/* Pages */}
+
+
+ {catalogue.nombre_pages} pages
+
+
+
+ ))}
+
+
+ {/* Pagination */}
+ {pagination.total > pagination.limit && (
+
+
+
+ Page {pagination.page} sur {Math.ceil(pagination.total / pagination.limit)}
+
+
+
+ )}
+ >
+ )}
+
+ {/* Catalogue Viewer Modal */}
+ {selectedCatalogue && (
+
+
+ {/* Header */}
+
+
+
{selectedCatalogue.titre}
+
+ {selectedCatalogue.enseigne.nom} • {cataloguePages.length} pages
+
+
+
+
+
+ {/* Pages Viewer */}
+
+ {cataloguePages.length === 0 ? (
+
+
+
+ ) : (
+
+ {cataloguePages.map(page => (
+
window.open(page.image_url, '_blank')}
+ >
+

+
+ Page {page.numero_page}
+
+
+ ))}
+
+ )}
+
+
+
+ )}
+
+ );
+}
diff --git a/setup_bonial.ps1 b/setup_bonial.ps1
new file mode 100644
index 0000000..ddc8e2b
--- /dev/null
+++ b/setup_bonial.ps1
@@ -0,0 +1,72 @@
+# Setup and Migration Script for Bonial Module
+#
+# Run this script to set up the Bonial catalog module
+
+Write-Host "================================================================" -ForegroundColor Cyan
+Write-Host " BONIAL CATALOG MODULE - SETUP SCRIPT" -ForegroundColor Cyan
+Write-Host "================================================================" -ForegroundColor Cyan
+Write-Host ""
+
+# Check if Docker is running
+Write-Host "[1/3] Checking Docker..." -ForegroundColor Yellow
+try {
+ $dockerCheck = docker ps 2>&1
+ if ($LASTEXITCODE -ne 0) {
+ Write-Host " ERROR: Docker is not running!" -ForegroundColor Red
+ Write-Host " Please start Docker Desktop and try again." -ForegroundColor Red
+ exit 1
+ }
+ Write-Host " OK - Docker is running" -ForegroundColor Green
+} catch {
+ Write-Host " ERROR: Docker not found!" -ForegroundColor Red
+ Write-Host " Please install Docker Desktop." -ForegroundColor Red
+ exit 1
+}
+
+# Run Alembic migration inside container
+Write-Host ""
+Write-Host "[2/3] Running database migration..." -ForegroundColor Yellow
+try {
+ docker compose exec -T priceflow alembic upgrade head
+ if ($LASTEXITCODE -eq 0) {
+ Write-Host " OK - Migration completed successfully" -ForegroundColor Green
+ } else {
+ Write-Host " WARNING: Migration may have failed" -ForegroundColor Yellow
+ Write-Host " Tables may already exist - this is normal" -ForegroundColor Yellow
+ }
+} catch {
+ Write-Host " ERROR: Could not run migration" -ForegroundColor Red
+ Write-Host " Error: $_" -ForegroundColor Red
+}
+
+# Restart application to trigger seeding
+Write-Host ""
+Write-Host "[3/3] Restarting application to seed enseignes..." -ForegroundColor Yellow
+try {
+ docker compose restart priceflow
+ if ($LASTEXITCODE -eq 0) {
+ Write-Host " OK - Application restarted" -ForegroundColor Green
+ Start-Sleep -Seconds 3
+ Write-Host " Check logs for: 'X enseigne(s) créée(s)'" -ForegroundColor Cyan
+ } else {
+ Write-Host " ERROR: Could not restart application" -ForegroundColor Red
+ }
+} catch {
+ Write-Host " ERROR: $_" -ForegroundColor Red
+}
+
+Write-Host ""
+Write-Host "================================================================" -ForegroundColor Cyan
+Write-Host " SETUP COMPLETE!" -ForegroundColor Green
+Write-Host "================================================================" -ForegroundColor Cyan
+Write-Host ""
+Write-Host "Next steps:" -ForegroundColor Yellow
+Write-Host " 1. Check application logs:" -ForegroundColor White
+Write-Host " docker compose logs -f priceflow" -ForegroundColor Gray
+Write-Host ""
+Write-Host " 2. Run verification script:" -ForegroundColor White
+Write-Host " docker compose exec priceflow python verify_bonial.py" -ForegroundColor Gray
+Write-Host ""
+Write-Host " 3. Test API endpoints:" -ForegroundColor White
+Write-Host " Invoke-WebRequest http://localhost:8555/api/catalogues/enseignes" -ForegroundColor Gray
+Write-Host ""
diff --git a/verify_bonial.py b/verify_bonial.py
new file mode 100644
index 0000000..90ba055
--- /dev/null
+++ b/verify_bonial.py
@@ -0,0 +1,294 @@
+"""
+Verification Script for Bonial Catalog Module
+
+Tests database setup, scraper functionality, and API endpoints.
+"""
+
+import asyncio
+import logging
+import sys
+from datetime import datetime
+
+import httpx
+from sqlalchemy import inspect, text
+
+from app.database import SessionLocal, engine
+from app.models import Catalogue, CataloguePage, Enseigne, ScrapingLog
+from app.services.bonial_scraper import scrape_enseigne
+from app.services.seed_enseignes import seed_enseignes
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+API_BASE_URL = "http://localhost:8555/api"
+
+
+def check_database_tables():
+ """Verify that catalog module tables exist."""
+ logger.info("=" * 60)
+ logger.info("Phase 1: Checking Database Tables")
+ logger.info("=" * 60)
+
+ inspector = inspect(engine)
+ required_tables = [
+ "enseignes",
+ "catalogues",
+ "catalogue_pages",
+ "scraping_logs",
+ ]
+
+ existing_tables = inspector.get_table_names()
+
+ all_exist = True
+ for table in required_tables:
+ exists = table in existing_tables
+ status = "✅" if exists else "❌"
+ logger.info(f"{status} Table '{table}': {'EXISTS' if exists else 'MISSING'}")
+ if not exists:
+ all_exist = False
+
+ if all_exist:
+ logger.info("\n✅ All required tables exist!\n")
+ return True
+ else:
+ logger.error("\n❌ Some tables are missing. Run migration: alembic upgrade head\n")
+ return False
+
+
+def check_enseignes_seeding():
+ """Verify that enseignes are seeded."""
+ logger.info("=" * 60)
+ logger.info("Phase 2: Checking Enseignes Seeding")
+ logger.info("=" * 60)
+
+ db = SessionLocal()
+ try:
+ count = db.query(Enseigne).count()
+ logger.info(f"Found {count} enseignes in database")
+
+ if count == 0:
+ logger.info("Seeding enseignes...")
+ created = seed_enseignes(db)
+ logger.info(f"✅ Created {created} enseignes")
+ count = created
+
+ if count >= 9:
+ logger.info("\n✅ All 9 enseignes are seeded!\n")
+
+ # Display enseignes
+ enseignes = db.query(Enseigne).order_by(Enseigne.ordre_affichage).all()
+ for ens in enseignes:
+ active = "✅" if ens.is_active else "⚠️"
+ logger.info(f" {active} {ens.ordre_affichage}. {ens.nom} (slug: {ens.slug_bonial})")
+
+ return True
+ else:
+ logger.warning(f"\n⚠️ Expected 9 enseignes, found {count}\n")
+ return False
+
+ finally:
+ db.close()
+
+
+async def test_scraper():
+ """Test the Bonial scraper on one enseigne."""
+ logger.info("=" * 60)
+ logger.info("Phase 3: Testing Bonial Scraper (Gifi)")
+ logger.info("=" * 60)
+
+ db = SessionLocal()
+ try:
+ # Get Gifi enseigne
+ gifi = db.query(Enseigne).filter_by(slug_bonial="Gifi").first()
+
+ if not gifi:
+ logger.error("❌ Gifi enseigne not found")
+ return False
+
+ logger.info(f"Testing scraper for: {gifi.nom}")
+ logger.info("This may take 30-60 seconds...")
+
+ # Run scraper
+ log = await scrape_enseigne(gifi, db)
+
+ # Display results
+ logger.info(f"\nScraping completed:")
+ logger.info(f" Status: {log.statut}")
+ logger.info(f" Catalogues found: {log.catalogues_trouves}")
+ logger.info(f" New catalogues: {log.catalogues_nouveaux}")
+ logger.info(f" Duration: {log.duree_secondes:.2f}s")
+
+ if log.message_erreur:
+ logger.warning(f" Error: {log.message_erreur}")
+
+ if log.statut in ["success", "partial"] and log.catalogues_trouves > 0:
+ logger.info("\n✅ Scraper is working!\n")
+
+ # Display sample catalog
+ cat = db.query(Catalogue).filter_by(enseigne_id=gifi.id).first()
+ if cat:
+ logger.info(f"Sample catalog:")
+ logger.info(f" Titre: {cat.titre}")
+ logger.info(f" Dates: {cat.date_debut.date()} → {cat.date_fin.date()}")
+ logger.info(f" Pages: {cat.nombre_pages}")
+
+ return True
+ else:
+ logger.error("\n❌ Scraper failed or found no catalogs\n")
+ return False
+
+ except Exception as e:
+ logger.error(f"❌ Error testing scraper: {e}")
+ return False
+ finally:
+ db.close()
+
+
+async def test_api_endpoints():
+ """Test API endpoints."""
+ logger.info("=" * 60)
+ logger.info("Phase 4: Testing API Endpoints")
+ logger.info("=" * 60)
+
+ async with httpx.AsyncClient(timeout=30.0) as client:
+ # Test 1: Get enseignes
+ logger.info("\n1. Testing GET /api/catalogues/enseignes")
+ try:
+ response = await client.get(f"{API_BASE_URL}/catalogues/enseignes")
+ if response.status_code == 200:
+ enseignes = response.json()
+ logger.info(f" ✅ Status 200 - Found {len(enseignes)} enseignes")
+ if enseignes:
+ logger.info(f" Sample: {enseignes[0]['nom']} ({enseignes[0]['catalogues_actifs_count']} catalogues)")
+ else:
+ logger.error(f" ❌ Status {response.status_code}")
+ return False
+ except Exception as e:
+ logger.error(f" ❌ Error: {e}")
+ return False
+
+ # Test 2: Get catalogues
+ logger.info("\n2. Testing GET /api/catalogues")
+ try:
+ response = await client.get(f"{API_BASE_URL}/catalogues?page=1&limit=5")
+ if response.status_code == 200:
+ data = response.json()
+ catalogues = data.get("data", [])
+ pagination = data.get("pagination", {})
+ logger.info(f" ✅ Status 200 - Found {pagination.get('total', 0)} catalogues")
+ logger.info(f" Page: {pagination.get('page')}/{pagination.get('pages_total')}")
+ if catalogues:
+ logger.info(f" Sample: {catalogues[0]['titre']}")
+ else:
+ logger.error(f" ❌ Status {response.status_code}")
+ return False
+ except Exception as e:
+ logger.error(f" ❌ Error: {e}")
+ return False
+
+ # Test 3: Get catalogue detail
+ logger.info("\n3. Testing GET /api/catalogues/{id}")
+ try:
+ # Get first catalog ID
+ db = SessionLocal()
+ cat = db.query(Catalogue).first()
+ db.close()
+
+ if cat:
+ response = await client.get(f"{API_BASE_URL}/catalogues/{cat.id}")
+ if response.status_code == 200:
+ detail = response.json()
+ logger.info(f" ✅ Status 200 - Catalogue: {detail['titre']}")
+ logger.info(f" Pages: {detail['nombre_pages']}")
+ else:
+ logger.error(f" ❌ Status {response.status_code}")
+ return False
+ else:
+ logger.warning(" ⚠️ No catalogues in DB to test detail endpoint")
+ except Exception as e:
+ logger.error(f" ❌ Error: {e}")
+ return False
+
+ # Test 4: Get catalogue pages
+ logger.info("\n4. Testing GET /api/catalogues/{id}/pages")
+ try:
+ if cat:
+ response = await client.get(f"{API_BASE_URL}/catalogues/{cat.id}/pages")
+ if response.status_code == 200:
+ pages = response.json()
+ logger.info(f" ✅ Status 200 - Found {len(pages)} pages")
+ if pages:
+ logger.info(f" Sample page: {pages[0]['numero_page']} - {pages[0]['image_url'][:50]}...")
+ else:
+ logger.error(f" ❌ Status {response.status_code}")
+ return False
+ except Exception as e:
+ logger.error(f" ❌ Error: {e}")
+ return False
+
+ # Test 5: Get stats (requires working DB)
+ logger.info("\n5. Testing GET /api/catalogues/admin/stats")
+ try:
+ response = await client.get(f"{API_BASE_URL}/catalogues/admin/stats")
+ if response.status_code == 200:
+ stats = response.json()
+ logger.info(f" ✅ Status 200 - Total catalogues: {stats['total_catalogues']}")
+ logger.info(f" Prochaine exécution: {stats['prochaine_execution']}")
+ else:
+ logger.error(f" ❌ Status {response.status_code}")
+ # Stats is optional, don't fail
+ except Exception as e:
+ logger.warning(f" ⚠️ Stats endpoint error (may require auth): {e}")
+
+ logger.info("\n✅ All API endpoints are working!\n")
+ return True
+
+
+async def main():
+ """Run all verification tests."""
+ logger.info("\n" + "=" * 60)
+ logger.info("BONIAL CATALOG MODULE - VERIFICATION SCRIPT")
+ logger.info("=" * 60 + "\n")
+
+ results = []
+
+ # Phase 1: Database tables
+ results.append(("Database Tables", check_database_tables()))
+
+ if not results[0][1]:
+ logger.error("\n❌ Database not ready. Please run: alembic upgrade head")
+ sys.exit(1)
+
+ # Phase 2: Enseignes seeding
+ results.append(("Enseignes Seeding", check_enseignes_seeding()))
+
+ # Phase 3: Scraper test (optional, can be slow)
+ scraper_test = input("\nRun scraper test? (Gifi - takes ~60s) [y/N]: ").lower() == "y"
+ if scraper_test:
+ results.append(("Scraper Test", await test_scraper()))
+
+ # Phase 4: API endpoints (requires app to be running)
+ api_test = input("\nTest API endpoints? (App must be running on :8555) [y/N]: ").lower() == "y"
+ if api_test:
+ results.append(("API Endpoints", await test_api_endpoints()))
+
+ # Summary
+ logger.info("\n" + "=" * 60)
+ logger.info("VERIFICATION SUMMARY")
+ logger.info("=" * 60)
+
+ for name, passed in results:
+ status = "✅ PASSED" if passed else "❌ FAILED"
+ logger.info(f"{status}: {name}")
+
+ all_passed = all(result[1] for result in results)
+
+ if all_passed:
+ logger.info("\n✅ ALL TESTS PASSED - Bonial module is ready!")
+ else:
+ logger.error("\n❌ SOME TESTS FAILED - Please review errors above")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())