-
-
-
-
-
-
+
+
+
+
+
+
Commande #
+
+
+
+
+
+
Statut
+
+
+
+
+
+
+
Détails
+
+ Type
+
+
+
+ Date
+
+
+
+ Note
+
+
+
+
+ Livreur
+
+
+
+
+
+
+
+
+
+ Sous-total
+
+
+
+ Frais de livraison
+
+
+
+ Pourboire
+
+
+
+
+ Total
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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