diff --git "a/index.html" "b/index.html" --- "a/index.html" +++ "b/index.html" @@ -1,92 +1,2942 @@ +```html - + - Pixel Palette Explorer - + PizzaExpress - Plateforme de Livraison - - - + - -
-
-

Pixel Palette Explorer

-

Discover the perfect color combinations for your next project

+ + +
+
+

PizzaExpress

+

Choisissez votre rôle pour continuer

+ +
+ + + + + + + +
-
-
-
-
-

Color Generator

-

Generate beautiful color palettes instantly

- - Explore - - + +
+
+

Accès Livreur

+

Entrez le code PIN pour continuer

+ +
+ + +
+
+
+ + +
+
+

Accès Admin

+

Entrez le code PIN pour continuer

+ +
+ +
+
+
-
-
-
-

Popular Palettes

-

Browse trending color combinations

- - Explore - - + +
+ +
+
+

PizzaExpress

+
+ +
+
+ + +
+
+
+ + +
+
+ +
+ + + + +
-
-
+
+ +
+
+ + +
+
+
+ +
-

Save Favorites

-

Create your personal color library

- - Explore - - +
+

+ +
+

+ + + +
+
+ + + +
+ +
+ +
+ + +
+
+
+
+ + +
+
+
+ +

Mon panier

+
+
+ + + + +
+
+ + +
+
+
+ +

Paiement

+
+
+ +
+

Informations personnelles

+
+ + + +
+
+ +
+

Pourboire

+
+ +
+ 0€ + + 10€ +
+
+
+ +
+

Récapitulatif

+
+
+ Sous-total + +
+
+ Frais de livraison + +
+
+ Pourboire + +
+
+
+ Total + +
+
+
+ + +
+
+ + +
+
+
+ +

Suivi de commande

+
+
+ +
+
+ +
+

Commande #

+

+
+ +
+
+
+ Temps estimé + +
+
+
+
+ +
+
+ +
+
+
+ +
+

Détails

+
+
+ Adresse + +
+
+ Livreur + +
+
+ Type + +
+
+ Note + +
+
+
+ +
+

Articles

+
+ +
+
+ + +
+
+ + +
+
+
+ +

Historique

+
+
+ +
+ +
+
+
+
+ + +
+ +
+
+

PizzaExpress - Cuisine

+
+ + +
+
+
+ + +
+
+

Commandes en cours

+
+ + + +
+
+ +
+ +
+
+

Nouveau

+ +
+
+ +
+
+ + +
+
+

Préparation

+ +
+
+ + +
+
+ + +
+
+

Prêt

+ +
+
+ +
+
+
+
+
+ + +
+ +
+
+

PizzaExpress - Livreur

+
+ + +
+
+
+ +
+ +
+
+
+

Statut

+

+
+ +
+
+ + +
+

Résumé du jour

+
+
+

Livraisons

+

+
+
+

Gains

+

+
+
+

Moyenne

+

+
+
+
+ + +
+

Livraisons en cours

+
+ +
+
+ + +
+
+ +
+

Aucune livraison en cours

+

Vous serez notifié lorsqu'une nouvelle livraison vous sera assignée

+
+
+ + +
+
+

Preuve de livraison

+
+ + +
+
+ + +
+
+
+
+ + +
+ +
+
+

PizzaExpress - Admin

+
+ + +
+
+
+ +
+ +
+

Tableau de bord

+ +
+
+

CA aujourd'hui

+

+
+
+

Commandes

+

+
+
+

Panier moyen

+

+
+
+

Taux annulation

+

+
+
+ +
+
+

Temps moyen de préparation

+

+
+
+

Temps moyen de livraison

+

+
+
+ +
+

Statut des commandes

+
+
+
+ +
+

Nouveau

+
+
+
+ +
+

Accepté

+
+
+
+ +
+

Préparation

+
+
+
+ +
+

Prêt

+
+
+
+ +
+

Récupéré

+
+
+
+ +
+

En cours

+
+
+
+ +
+

Livré

+
+
+
+ +
+

Livreurs

+
+ + + + + + + + + + + + +
NomStatutLivraisonsGains
+
+
+
+ + +
+
+

Commandes

+
+ + + +
+
+ +
+
+ + + + + + + + + + + + + + + +
IDClientStatutTypeTotalDateActions
+
+
+
+ + +
+
+

Gestion du menu

+ +
+ +
+
+ + + + + + + + + + + + + + +
ImageNomCatégoriePrixTagsActions
+
+
+
+ + +
+
+

Gestion des livreurs

+ +
+ +
+
+ + + + + + + + + + + + + + +
NomTéléphoneStatutLivraisonsGainsActions
+
+
+
+ + +
+

Dispatch

+ +
+ +
+

Commandes prêtes

+
+ + +
+
+ + +
+

Livreurs disponibles

+
+ + +
+
+ + +
+

Paramètres

+
+
+ + +
+
+ + +

Mettez 0 pour désactiver

+
+ +
+
+
+
+ + +
+
+
+ + + + + +
+
+
+
+
+ + +
+
+
+
+

+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ +
+ + + +
+
+ +
+ + +
+
+
+
+
+ + +
+
+
+
+

+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
-
-

Color Inspiration

-
-
-
-
-
-
-
+ +
+
+
+
+

Commande #

+ +
+ +
+
+

Statut

+ +
+ +
+

Client

+

+

+

+
+ +
+

Détails

+
+ Type + +
+
+ Date + +
+
+ Note + +
+ +
+ +
+

Articles

+
+ +
+
+ +
+
+ Sous-total + +
+
+ Frais de livraison + +
+
+ Pourboire + +
+
+
+ Total + +
+
+ +
+

Historique

+
+ +
+
+ +
+ + +
+
+
+
+
+ + +
+
+
+
+

Assigner livreur

+ +
+ +
+
+ + +
+ +
+ + +
+
-
+ + + +
+ +
+ + +
+
+
+
+

PizzaExpress - Mode démo

+ +
+ +
+

Fonctionnalités implémentées

+
    +
  • 4 interfaces distinctes: Client, Cuisine, Livreur, Admin
  • +
  • Persistance des données via localStorage
  • +
  • Simulation de mise à jour en temps réel entre les vues
  • +
  • Système complet de commande avec statuts et workflow
  • +
  • Gestion du menu et des livreurs
  • +
  • Tableau de bord avec indicateurs clés
  • +
  • Design responsive mobile-first
  • +
+ +

Données de démo

+

L'application utilise des données mockées stockées dans localStorage:

+
    +
  • 10 pizzas, 4 boissons, 4 desserts avec des noms et prix réalistes
  • +
  • 3 livreurs avec des informations de contact
  • +
  • Commandes d'exemple avec différents statuts
  • +
+ +

Accès aux interfaces protégées

+
    +
  • Livreur: PIN = 1234
  • +
  • Admin: PIN = 1234
  • +
+ +

Limitations

+
    +
  • Pas d'authentification réelle (mock seulement)
  • +
  • Pas de backend réel - toutes les données sont stockées localement
  • +
  • Pas de paiement réel simulé
  • +
  • Les images utilisent des placeholders
  • +
+ +
+ +
+
+
+
+
+ + + - - - - \ No newline at end of file + document.addEventListener('alpine:init', () => { + Alpine.data('app', () => ({ + // App state + currentScreen: 'role-select', + currentRole: null, + showCourierPin: false, + courierPin: '', + showBossPin: false, + bossPin: '', + showReadme: false, + + // Client state + searchQuery: '', + activeCategory: 'all', + selectedProduct: null, + productOptions: { + size: 'M', + dough: 'fine', + toppings: [], + quantity: 1 + }, + cart: { + items: [], + subtotal: 0, + deliveryFee: 3.5, + tip: 2, + total: 0, + note: '', + deliveryType: 'livraison' + }, + clientName: '', + clientPhone: '', + clientAddress: '', + currentOrder: null, + trackingProgress: 0, + trackingSteps: [ + { label: 'Commande passée', status: 'placed', icon: 'shopping-cart' }, + { label: 'Acceptée', status: 'accepted', icon: 'check' }, + { label: 'En préparation', status: 'preparing', icon: 'chef-hat' }, + { label: 'Prête', status: 'ready', icon: 'package-check' }, + { label: 'Récupérée', status: 'picked_up', icon: 'package-open' }, + { label: 'En cours de livraison', status: 'on_the_way', icon: 'bike' }, + { label: 'Livrée', status: 'delivered', icon: 'package' } + ], + + // Cuisine state + kitchenFilter: 'all', + + // Livreur state + currentCourier: null, + showDeliveryProof: null, + deliveryProofNote: '', + + // Admin state + adminTab: 'dashboard', + orderFilter: { + status: 'all', + type: 'all' + }, + dispatchSettings: { + deliveryFee: 3.5, + freeDeliveryThreshold: 25 + }, + showAdminProductModal: false, + editingProduct: null, + productForm: { + id: '', + name: '', + category: 'pizza', + price: 0, + description: '', + image: '', + tags: [] + }, + showCourierModal: false, + editingCourier: null, + courierForm: { + id: '', + name: '', + phone: '', + isAvailable: true + }, + showOrderDetailsModal: false, + selectedOrder: null, + showAssignCourierModal: false, + assignOrderId: null, + assignCourierId: null, + + // Shared state + currentTime: '', + toasts: [], + + // Data models + products: [ + { + id: 'p1', + name: 'Margherita', + category: 'pizza', + price: 8.5, + description: 'Sauce tomate, mozzarella, basilic', + image: 'http://static.photos/food/640x360/1', + tags: ['vegetarian'] + }, + { + id: 'p2', + name: 'Pepperoni', + category: 'pizza', + price: 10.5, + description: 'Sauce tomate, mozzarella, pepperoni', + image: 'http://static.photos/food/640x360/2', + tags: [] + }, + { + id: 'p3', + name: '4 Fromages', + category: 'pizza', + price: 11.5, + description: 'Sauce tomate, mozzarella, gorgonzola, parmesan, chèvre', + image: 'http://static.photos/food/640x360/3', + tags: ['vegetarian', 'best-seller'] + }, + { + id: 'p4', + name: 'Végétarienne', + category: 'pizza', + price: 10.0, + description: 'Sauce tomate, mozzarella, poivrons, oignons, olives, champignons', + image: 'http://static.photos/food/640x360/4', + tags: ['vegetarian'] + }, + { + id: 'p5', + name: 'Poulet BBQ', + category: 'pizza', + price: 11.0, + description: 'Sauce BBQ, mozzarella, poulet, oignons rouges', + image: 'http://static.photos/food/640x360/5', + tags: [] + }, + { + id: 'p6', + name: 'Calzone', + category: 'pizza', + price: 12.0, + description: 'Sauce tomate, mozzarella, jambon, champignons, pliée en deux', + image: 'http://static.photos/food/640x360/6', + tags: [] + }, + { + id: 'p7', + name: 'Hawaïenne', + category: 'pizza', + price: 10.5, + description: 'Sauce tomate, mozzarella, jambon, ananas', + image: 'http://static.photos/food/640x360/7', + tags: [] + }, + { + id: 'p8', + name: 'Diavola', + category: 'pizza', + price: 11.5, + description: 'Sauce tomate, mozzarella, salami piquant, piments', + image: 'http://static.photos/food/640x360/8', + tags: ['spicy'] + }, + { + id: 'p9', + name: 'Truffée', + category: 'pizza', + price: 13.5, + description: 'Crème fraîche, mozzarella, champignons, truffe', + image: 'http://static.photos/food/640x360/9', + tags: ['best-seller'] + }, + { + id: 'p10', + name: 'Reine', + category: 'pizza', + price: 11.0, + description: 'Sauce tomate, mozzarella, jambon, champignons', + image: 'http://static.photos/food/640x360/10', + tags: [] + }, + { + id: 'b1', + name: 'Coca-Cola', + category: 'boisson', + price: 2.5, + description: '33cl', + image: 'http://static.photos/food/320x240/11', + tags: [] + }, + { + id: 'b2', + name: 'Eau minérale', + category: 'boisson', + price: 2.0, + description: '50cl', + image: 'http://static.photos/food/320x240/12', + tags: [] + }, + { + id: 'b3', + name: 'Jus d\'orange', + category: 'boisson', + price: 3.0, + description: '25cl pressé', + image: 'http://static.photos/food/320x240/13', + tags: [] + }, + { + id: 'b4', + name: 'Perrier', + category: 'boisson', + price: 2.5, + description: '33cl', + image: 'http://static.photos/food/320x240/14', + tags: [] + }, + { + id: 'd1', + name: 'Tiramisu', + category: 'dessert', + price: 5.0, + description: 'Dessert italien au café', + image: 'http://static.photos/food/320x240/15', + tags: [] + }, + { + id: 'd2', + name: 'Panna Cotta', + category: 'dessert', + price: 4.5, + description: 'Crème dessert vanille et fruits rouges', + image: 'http://static.photos/food/320x240/16', + tags: [] + }, + { + id: 'd3', + name: 'Fondant au chocolat', + category: 'dessert', + price: 5.5, + description: 'Cœur coulant, glace vanille', + image: 'http://static.photos/food/320x240/17', + tags: ['best-seller'] + }, + { + id: 'd4', + name: 'Salade de fruits', + category: 'dessert', + price: 4.0, + description: 'Fruits de saison', + image: 'http://static.photos/food/320x240/18', + tags: [] + } + ], + orders: [], + couriers: [ + { + id: 'c1', + name: 'Jean Dupont', + phone: '0612345678', + isAvailable: true, + activeOrderIds: [], + earningsToday: 0, + deliveriesToday: 0 + }, + { + id: 'c2', + name: 'Marie Martin', + phone: '0698765432', + isAvailable: true, + activeOrderIds: [], + earningsToday: 0, + deliveriesToday: 0 + }, + { + id: 'c3', + name: 'Pierre Bernard', + phone: '0654321789', + isAvailable: false, + activeOrderIds: [], + earningsToday: 0, + deliveriesToday: 0 + } + ], + restaurantAddress: '15 Rue de la Pizza, 75001 Paris', + + // Initialize app + initApp() { + // Load data from localStorage + this.loadData(); + + // Set current time and update every minute + this.updateTime(); + setInterval(() => this.updateTime(), 60000); + + // Check for existing session + const savedRole = localStorage.getItem('currentRole'); + if (savedRole) { + this.currentRole = savedRole; + if (savedRole === 'client') { + this.currentScreen = 'home'; + } else if (savedRole === 'livreur') { + this.currentCourier = this.couriers.find(c => c.id === localStorage.getItem('currentCourierId')) || this.couriers[0]; + } + } + + // Listen for storage events (simulate realtime updates) + window.addEventListener('storage', (e) => { + if (e.key === 'orders') { + this.orders = JSON.parse(e.newValue || '[]'); + } else if (e.key === 'couriers') { + this.couriers = JSON.parse(e.newValue || '[]'); + if (this.currentRole === 'livreur') { + this.currentCourier = this.couriers.find(c => c.id === this.currentCourier?.id); + } + } + }); + + // Initialize icons + lucide.createIcons(); + + // Show README on first visit + const firstVisit = !localStorage.getItem('visited'); + if (firstVisit) { + this.showReadme = true; + localStorage.setItem('visited', 'true'); + } + }, + + // Data loading/saving + loadData() { + const savedProducts = localStorage.getItem('products'); + if (savedProducts) this.products = JSON.parse(savedProducts); + + const savedOrders = localStorage.getItem('orders'); + if (savedOrders) this.orders = JSON.parse(savedOrders); + + const savedCouriers = localStorage.getItem('couriers'); + if (savedCouriers) this.couriers = JSON.parse(savedCouriers); + + const savedSettings = localStorage.getItem('dispatchSettings'); + if (savedSettings) this.dispatchSettings = JSON.parse(savedSettings); + + // Initialize cart if empty + if (this.orders.length === 0) { + this.initializeDemoData(); + } + }, + + saveData() { + localStorage.setItem('products', JSON.stringify(this.products)); + localStorage.setItem('orders', JSON.stringify(this.orders)); + localStorage.setItem('couriers', JSON.stringify(this.couriers)); + localStorage.setItem('dispatchSettings', JSON.stringify(this.dispatchSettings)); + + // Trigger storage event to sync across tabs + window.dispatchEvent(new Event('storage')); + }, + + initializeDemoData() { + // Add some demo orders + const demoOrders = [ + { + id: this.generateId(), + createdAt: new Date(Date.now() - 3600000).toISOString(), + customer: { + name: 'Sophie Laurent', + phone: '0654321890', + address: '22 Rue des Fleurs, 75010 Paris' + }, + items: [ + { + product: this.products[0], + options: { size: 'M', dough: 'fine', toppings: [], quantity: 1 }, + quantity: 1 + }, + { + product: this.products[3], + options: { size: 'L', dough: 'classique', toppings: ['fromage', 'olives'], quantity: 1 }, + quantity: 1 + } + ], + notes: 'Sans oignons sur la végétarienne', + deliveryType: 'livraison', + totals: { + subtotal: 19.0, + deliveryFee: 3.5, + tip: 2, + total: 24.5 + }, + status: 'delivered', + assignedCourierId: 'c1', + timeline: [ + { status: 'placed', timestamp: new Date(Date.now() - 3600000).toISOString(), actor: 'client' }, + { status: 'accepted', timestamp: new Date(Date.now() - 3540000).toISOString(), actor: 'cuisine' }, + { status: 'preparing', timestamp: new Date(Date.now() - 3480000).toISOString(), actor: 'cuisine' }, + { status: 'ready', timestamp: new Date(Date.now() - 3300000).toISOString(), actor: 'cuisine' }, + { status: 'picked_up', timestamp: new Date(Date.now() - 3240000).toISOString(), actor: 'livreur' }, + { status: 'on_the_way', timestamp: new Date(Date.now() - 3180000).toISOString(), actor: 'livreur' }, + { status: 'delivered', timestamp: new Date(Date.now() - 3000000).toISOString(), actor: 'livreur' } + ], + etaMinutes: 45 + }, + { + id: this.generateId(), + createdAt: new Date(Date.now() - 1800000).toISOString(), + customer: { + name: 'Thomas Dubois', + phone: '0678912345', + address: '5 Avenue Mozart, 75016 Paris' + }, + items: [ + { + product: this.products[8], + options: { size: 'M', dough: 'fine', toppings: [], quantity: 1 }, + quantity: 1 + }, + { + product: this.products[15], + options: { size: 'S', dough: 'classique', toppings: [], quantity: 1 }, + quantity: 1 + } + ], + notes: '', + deliveryType: 'a emporter', + totals: { + subtotal: 18.5, + deliveryFee: 0, + tip: 1.5, + total: 20.0 + }, + status: 'picked_up', + assignedCourierId: null, + timeline: [ + { status: 'placed', timestamp: new Date(Date.now() - 1800000).toISOString(), actor: 'client' }, + { status: 'accepted', timestamp: new Date(Date.now() - 1740000).toISOString(), actor: 'cuisine' }, + { status: 'preparing', timestamp: new Date(Date.now() - 1680000).toISOString(), actor: 'cuisine' }, + { status: 'ready', timestamp: new Date(Date.now() - 1560000).toISOString(), actor: 'cuisine' }, + { status: 'picked_up', timestamp: new Date(Date.now() - 1500000).toISOString(), actor: 'client' } + ], + etaMinutes: 30 + }, + { + id: this.generateId(), + createdAt: new Date(Date.now() - 600000).toISOString(), + customer: { + name: 'Laura Petit', + phone: '0698761234', + address: '12 Rue de Rivoli, 75004 Paris' + }, + items: [ + { + product: this.products[2], + options: { size: 'L', dough: 'fine', toppings: [], quantity: 1 }, + quantity: 1 + } + ], + notes: '', + deliveryType: 'livraison', + totals: { + subtotal: 11.5, + deliveryFee: 3.5, + tip: 1, + total: 16.0 + }, + status: 'ready', + assignedCourierId: null, + timeline: [ + { status: 'placed', timestamp: new Date(Date.now() - 600000).toISOString(), actor: 'client' }, + { status: 'accepted', timestamp: new Date(Date.now() - 540000).toISOString(), actor: 'cuisine' }, + { status: 'preparing', timestamp: new Date(Date.now() - 510000).toISOString(), actor: 'cuisine' }, + { status: 'ready', timestamp: new Date(Date.now() - 420000).toISOString(), actor: 'cuisine' } + ], + etaMinutes: 25 + }, + { + id: this.generateId(), + createdAt: new Date(Date.now() - 300000).toISOString(), + customer: { + name: 'Marc Leroy', + phone: '0612349876', + address: '8 Boulevard Saint-Germain, 75005 Paris' + }, + items: [ + { + product: this.products[5], + options: { size: 'M', dough: 'classique', toppings: [], quantity: 1 }, + quantity: 1 + }, + { + product: this.products[10], + options: { size: 'S', dough: 'classique', toppings: [], quantity: 1 }, + quantity: 1 + } + ], + notes: 'Couper la calzone en 4', + deliveryType: 'livraison', + totals: { + subtotal: 14.5, + deliveryFee: 3.5, + tip: 2, + total: 20.0 + }, + status: 'preparing', + assignedCourierId: null, + timeline: [ + { status: 'placed', timestamp: new Date(Date.now() - 300000).toISOString(), actor: 'client' }, + { status: 'accepted', timestamp: new Date(Date.now() - 270000).toISOString(), actor: 'cuisine' }, + { status: 'preparing', timestamp: new Date(Date.now() - 240000).toISOString(), actor: 'cuisine' } + ], + etaMinutes: 35 + } + ]; + + this.orders = demoOrders; + this.saveData(); + }, + + // Role management + login(role) { + this.currentRole = role; + localStorage.setItem('currentRole', role); + + if (role === 'client') { + this.currentScreen = 'home'; + } else if (role === 'cuisine') { + this.currentScreen = 'home'; + } else if (role === 'livreur') { + this.currentCourier = this.couriers[0]; + localStorage.setItem('currentCourierId', this.currentCourier.id); + this.currentScreen = 'home'; + } else if (role === 'boss') { + this.currentScreen = 'dashboard'; + this.adminTab = 'dashboard'; + } + }, + + logout() { + this.currentRole = null; + this.currentScreen = 'role-select'; + localStorage.removeItem('currentRole'); + if (this.currentCourier) { + localStorage.removeItem('currentCourierId'); + this.currentCourier = null; + } + }, + + // Client functions + get filteredProducts() { + let filtered = this.products; + + // Filter by search query + if (this.searchQuery) { + const query = this.searchQuery.toLowerCase(); + filtered = filtered.filter(p => + p.name.toLowerCase().includes(query) || + p.description.toLowerCase().includes(query) + ); + } + + // Filter by category + if (this.activeCategory !== 'all') { + filtered = filtered.filter(p => p.category === this.activeCategory); + } + + return filtered; + }, + + openProductModal(product) { + this.selectedProduct = product; + this.currentScreen = 'product-modal'; + // Reset options when opening modal + this.productOptions = { + size: 'M', + dough: 'fine', + toppings: [], + quantity: 1 + }; + }, + + calculateProductTotal() { + let total = this.selectedProduct.price; + + // Adjust for size + if (this.selectedProduct.category === 'pizza') { + if (this.productOptions.size === 'M') { + total += 1; + } else if (this.productOptions.size === 'L') { + total += 2; + } + + // Add toppings + total += this.productOptions.toppings.length * 0.8; + } + + return total * this.productOptions.quantity; + }, + + addToCart() { + const totalPrice = this.calculateProductTotal(); + const cartItem = { + product: this.selectedProduct, + options: { ...this.productOptions }, + quantity: this.productOptions.quantity, + itemPrice: totalPrice / this.productOptions.quantity + }; + + // Check if similar item already exists in cart + let exists = false; + this.cart.items = this.cart.items.map(item => { + if (this.isSameCartItem(item, cartItem)) { + exists = true; + return { + ...item, + quantity: item.quantity + cartItem.quantity + }; + } + return item; + }); + + if (!exists) { + this.cart.items.push(cartItem); + } + + this.calculateCartTotals(); + this.currentScreen = 'home'; + this.toastMessage('Article ajouté au panier', 'success'); + }, + + isSameCartItem(item1, item2) { + if (item1.product.id !== item2.product.id) return false; + + // For pizzas, check all options + if (item1.product.category === 'pizza') { + return ( + item1.options.size === item2.options.size && + item1.options.dough === item2.options.dough && + JSON.stringify(item1.options.toppings) === JSON.stringify(item2.options.toppings) + ); + } + + return true; + }, + + updateCartItemQuantity(index, change) { + const newQuantity = this.cart.items[index].quantity + change; + + if (newQuantity <= 0) { + this.removeFromCart(index); + } else { + this.cart.items[index].quantity = newQuantity; + this.calculateCartTotals(); + } + }, + + removeFromCart(index) { + this.cart.items.splice(index, 1); + this.calculateCartTotals(); + }, + + calculateCartTotals() { + this.cart.subtotal = this.cart.items.reduce((sum, item) => sum + (item.itemPrice * item.quantity), 0); + + // Calculate delivery fee + if (this.cart.deliveryType === 'livraison') { + if (this.dispatchSettings.freeDeliveryThreshold > 0 && + this.cart.subtotal >= this.dispatchSettings.freeDeliveryThreshold) { + this.cart.deliveryFee = 0; + } else { + this.cart.deliveryFee = this.dispatchSettings.deliveryFee; + } + } else { + this.cart.deliveryFee = 0; + } + + this.cart.total = this.cart.subtotal + this.cart.deliveryFee + this.cart.tip; + }, + + placeOrder() { + if (!this.clientName || !this.clientPhone || (this.cart.deliveryType === 'livraison' && !this.clientAddress)) { + this.toastMessage('Veuillez remplir tous les champs requis', 'error'); + return; + } + + const newOrder = { + id: this.generateId(), + createdAt: new Date().toISOString(), + customer: { + name: this.clientName, + phone: this.clientPhone, + address: this.clientAddress || this.restaurantAddress + }, + items: this.cart.items.map(item => ({ + product: item.product, + options: { ...item.options }, + quantity: item.quantity + })), + notes: this.cart.note, + deliveryType: this.cart.deliveryType, + totals: { + subtotal: this.cart.subtotal, + deliveryFee: this.cart.deliveryFee, + tip: this.cart.tip, + total: this.cart.total + }, + status: 'placed', + assignedCourierId: null, + timeline: [ + { status: 'placed', timestamp: new Date().toISOString(), actor: 'client' } + ], + etaMinutes: Math.floor(Math.random() * 20) + 20 // Random ETA 20-40 min + }; + + this.orders.push(newOrder); + this.saveData(); + + // Reset cart + this.cart = { + items: [], + subtotal: 0, + deliveryFee: 3.5, + tip: 2, + total: 0, + note: '', + deliveryType: 'livraison' + }; + + // Show tracking + this.currentOrder = newOrder; + this.updateTrackingProgress(); + this.currentScreen = 'order-tracking'; + + this.toastMessage('Commande passée avec succès', 'success'); + }, + + viewOrder(orderId) { + this.currentOrder = this.orders.find(o => o.id === orderId); + this.updateTrackingProgress(); + this.currentScreen = 'order-tracking'; + }, + + updateTrackingProgress() { + const statusOrder = ['placed', 'accepted', 'preparing', 'ready', 'picked_up', 'on_the_way', 'delivered']; + this.trackingProgress = statusOrder.indexOf(this.currentOrder.status) + 1; + }, + + // Cuisine functions + get kitchenOrders() { + return this.orders.filter(o => + o.status !== 'delivered' && + o.status !== 'canceled' + ); + }, + + filteredKitchenOrders(status) { + let filtered = this.orders.filter(o => o.status === status); + + if (this.kitchenFilter !== 'all') { + filtered = filtered.filter(o => o.deliveryType === this.kitchenFilter); + } + + return filtered.sort((a, b) => + new Date(a.timeline.find(t => t.status === status).timestamp) - + new Date(b.timeline.find(t => t.status === status).timestamp) + ); + }, + + updateOrderStatus(orderId, newStatus) { + const order = this.orders.find(o => o.id === orderId); + if (!order) return; + + order.status = newStatus; + order.timeline.push({ + status: newStatus, + timestamp: new Date().toISOString(), + actor: this.currentRole + }); + + // For courier, update earnings when delivered + if (newStatus === 'delivered' && this.currentRole === 'livreur' && this.currentCourier) { + this.currentCourier.earningsToday += order.totals.tip + 2; // Tip + fixed fee + this.currentCourier.deliveriesToday += 1; + + // Remove from active orders + this.currentCourier.activeOrderIds = this.currentCourier.activeOrderIds.filter(id => id !== orderId); + } + + // For admin, assign courier if status is ready and delivery + if (newStatus === 'ready' && order.deliveryType === 'livraison' && this.currentRole === 'boss') { + this.assignOrderId = orderId; + this.showAssignCourierModal = true; + } + + this.saveData(); + this.toastMessage(`Statut de la commande #${orderId} mis à jour`, 'success'); + + // Play sound for new orders in kitchen + if (this.currentRole === 'cuisine' && newStatus === 'placed') { + this.playSound(); + } + }, + + assignCourier(orderId, courierId) { + const order = this.orders.find(o => o.id === orderId); + const courier = this.couriers.find(c => c.id === courierId); + + if (order && courier) { + order.assignedCourierId = courierId; + order.timeline.push({ + status: 'ready', + timestamp: new Date().toISOString(), + actor: 'boss' + }); + + // Add to courier's active orders + if (!courier.activeOrderIds.includes(orderId)) { + courier.activeOrderIds.push(orderId); + } + + this.saveData(); + this.showAssignCourierModal = false; + this.toastMessage(`Livreur assigné à la commande #${orderId}`, 'success'); + } + }, + + // Livreur functions + toggleAvailability() { + if (this.currentCourier) { + this.currentCourier.isAvailable = !this.currentCourier.isAvailable; + this.saveData(); + + const status = this.currentCourier.isAvailable ? 'disponible' : 'indisponible'; + this.toastMessage(`Vous êtes maintenant ${status}`, 'success'); + } + }, + + get activeCourierOrders() { + if (!this.currentCourier) return []; + return this.orders.filter(o => + this.currentCourier.activeOrderIds.includes(o.id) && + o.status !== 'delivered' && + o.status !== 'canceled' + ); + }, + + completeDelivery(orderId) { + this.updateOrderStatus(orderId, 'delivered'); + + // Add proof of delivery + const order = this.orders.find(o => o.id === orderId); + if (order) { + if (!order.timeline.some(t => t.note)) { + order.timeline.push({ + status: 'delivered', + timestamp: new Date().toISOString(), + actor: 'livreur', + note: this.deliveryProofNote || 'Livré avec succès' + }); + } + } + + this.showDeliveryProof = null; + this.deliveryProofNote = ''; + }, + + // Admin functions + get dashboardData() { + const todayOrders = this.orders.filter(o => + new Date(o.createdAt).toDateString() === new Date().toDateString() + ); + + const deliveredOrders = todayOrders.filter(o => o.status === 'delivered'); + const canceledOrders = todayOrders.filter(o => o.status === 'canceled'); + + // Calculate average prep time (placed -> ready) + const prepTimes = deliveredOrders.map(o => { + const placed = o.timeline.find(t => t.status === 'placed'); + const ready = o.timeline.find(t => t.status === 'ready'); + if (placed && ready) { + return (new Date(ready.timestamp) - new Date(placed.timestamp)) / 60000; // minutes + } + return 0; + }).filter(t => t > 0); + + const avgPrepTime = prepTimes.length > 0 ? + Math.round(prepTimes.reduce((a, b) => a + b, 0) / prepTimes.length) : 20; + + // Calculate average delivery time (ready -> delivered) + const deliveryTimes = deliveredOrders.filter(o => o.deliveryType === 'livraison').map(o => { + const ready = o.timeline.find(t => t.status === 'ready'); + const delivered = o.timeline.find(t => t.status === 'delivered'); + if (ready && delivered) { + return (new Date(delivered.timestamp) - new Date(ready.timestamp)) / 60000; // minutes + } + return 0; + }).filter(t => t > 0); + + const avgDeliveryTime = deliveryTimes.length > 0 ? + Math.round(deliveryTimes.reduce((a, b) => a + b, 0) / deliveryTimes.length) : 25; + + // Status counts + const statusCounts = { + placed: this.orders.filter(o => o.status === 'placed').length, + accepted: this.orders.filter(o => o.status === 'accepted').length, + preparing: this.orders.filter(o => o.status === 'preparing').length, + ready: this.orders.filter(o => o.status === 'ready').length, + picked_up: this.orders.filter(o => o.status === 'picked_up').length, + on_the_way: this.orders.filter(o => o.status === 'on_the_way').length, + delivered: deliveredOrders.length + }; + + return { + revenue: deliveredOrders.reduce((sum, o) => sum + o.totals.total, 0), + orderCount: todayOrders.length, + canceledOrders: canceledOrders.length, + avgPrepTime, + avgDeliveryTime, + statusCounts + }; + }, + + get filteredAdminOrders() { + let filtered = [...this.orders].reverse(); // Show newest first + + if (this.orderFilter.status !== 'all') { + filtered = filtered.filter(o => o.status === this.orderFilter.status); + } + + if (this.orderFilter.type !== 'all') { + filtered = filtered.filter(o => o.deliveryType === this.orderFilter.type); + } + + return filtered; + }, + + get ordersReadyForDispatch() { + return this.orders.filter(o => + o.status === 'ready' && + (o.deliveryType === 'livraison' ? !o.assignedCourierId : true) + ); + }, + + get availableCouriers() { + return this.couriers.filter(c => c.isAvailable); + }, + + openProductModal(product) { + this.editingProduct = product; + this.productForm = product ? + { ...product, tags: [...product.tags] } : + { + id: this.generateId(), + name: '', + category: 'pizza', + price: 0, + description: '', + image: '', + tags: [] + }; + this.showAdminProductModal = true; + }, + + saveProduct() { + if (!this.productForm.name || this.productForm.price <= 0) { + this.toastMessage('Veuillez remplir tous les champs requis', 'error'); + return; + } + + const product = { + ...this.productForm, + tags: Array.isArray(this.productForm.tags) ? + this.productForm.tags : + Object.entries(this.productForm.tags) + .filter(([_, value]) => value) + .map(([key]) => key) + }; + + const index = this.products.findIndex(p => p.id === product.id); + if (index >= 0) { + this.products[index] = product; + } else { + this.products.push(product); + } + + this.saveData(); + this.showAdminProductModal = false; + this.toastMessage('Produit enregistré', 'success'); + }, + + deleteProduct(productId) { + if (confirm('Supprimer ce produit ? Cette action est irréversible.')) { + this.products = this.products.filter(p => p.id !== productId); + this.saveData(); + this.toastMessage('Produit supprimé', 'success'); + } + }, + + openCourierModal(courier) { + this.editingCourier = courier; + this.courierForm = courier ? + { ...courier } : + { + id: this.generateId(), + name: '', + phone: '', + isAvailable: true + }; + this.showCourierModal = true; + }, + + saveCourier() { + if (!this.courierForm.name || !this.courierForm.phone) { + this.toastMessage('Veuillez remplir tous les champs requis', 'error'); + return; + } + + const courier = { ...this.courierForm }; + + const index = this.couriers.findIndex(c => c.id === courier.id); + if (index >= 0) { + this.couriers[index] = courier; + } else { + this.couriers.push(courier); + } + + this.saveData(); + this.showCourierModal = false; + this.toastMessage('Livreur enregistré', 'success'); + }, + + deleteCourier(courierId) { + if (confirm('Supprimer ce livreur ? Cette action est irréversible.')) { + this.couriers = this.couriers.filter(c => c.id !== courierId); + this.saveData(); + this.toastMessage('Livreur supprimé', 'success'); + } + }, + + toggleCourierAvailability(courierId) { + const courier = this.couriers.find(c => c.id === courierId); + if (courier) { + courier.isAvailable = !courier.isAvailable; + this.saveData(); + + const status = courier.isAvailable ? 'disponible' : 'indisponible'; + this.toastMessage(`Livreur marqué comme ${status}`, 'success'); + } + }, + + viewOrderDetails(orderId) { + this.selectedOrder = this.orders.find(o => o.id === orderId); + this.showOrderDetailsModal = true; + }, + + cancelOrder(orderId) { + if (confirm('Annuler cette commande ? Cette action est irréversible.')) { + const order = this.orders.find(o => o.id === orderId); + if (order) { + order.status = 'canceled'; + order.timeline.push({ + status: 'canceled', + timestamp: new Date().toISOString(), + actor: 'boss' + }); + + // Remove from courier's active orders if assigned + if (order.assignedCourierId) { + const courier = this.couriers.find(c => c.id === order.assignedCourierId); + if (courier) { + courier.activeOrderIds = courier.activeOrderIds.filter(id => id !== orderId); + } + } + + this.saveData(); + this.showOrderDetailsModal = false; + this.toastMessage('Commande annulée', 'success'); + } + } + }, + + saveDispatchSettings() { + this.saveData(); + this.toastMessage('Paramètres enregistrés', 'success'); + }, + + autoAssignOrders(courierId) { + const readyOrders = this.ordersReadyForDispatch + .filter(o => o.deliveryType === 'livraison') + .slice(0, 2); // Assign max 2 orders at once + + if (readyOrders.length === 0) { + this.toastMessage('Aucune commande à assigner', 'info'); + return; + } + + readyOrders.forEach(order => { + order.assignedCourierId = courierId; + order.timeline.push({ + status: 'ready', + timestamp: new Date().toISOString(), + actor: 'boss' + }); + + const courier = this.couriers.find(c => c.id === courierId); + if (courier && !courier.activeOrderIds.includes(order.id)) { + courier.activeOrderIds.push(order.id); + } + }); + + this.saveData(); + this.toastMessage(`${readyOrders.length} commande(s) assignée(s)`, 'success'); + }, + + exportToCSV() { + const headers = ['ID', 'Client', 'Téléphone', 'Adresse', 'Statut', 'Type', 'Total', 'Date']; + const data = this.filteredAdminOrders.map(order => [ + order.id, + order.customer.name, + order.customer.phone, + order.customer.address, + this.getStatusLabel(order.status), + order.deliveryType === 'livraison' ? 'Livraison' : 'À emporter', + `${order.totals.total.toFixed +___METADATA_START___ +{"repoId":"Dofla/pixel-palette-explorer","isNew":false,"userName":"Dofla"} +___METADATA_END___ \ No newline at end of file