'use client'; import React, { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Button, Card, Input, Select, Textarea } from '../../../shared/components'; import { useToast } from '@hykocx/zen/toast'; import { formatDateForInput, subtractDays, getTodayString } from '../../../shared/lib/dates.js'; /** * Invoice Create Page Component * Page for creating a new invoice */ const InvoiceCreatePage = ({ user }) => { const router = useRouter(); const toast = useToast(); const today = getTodayString(); const [clients, setClients] = useState([]); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [formData, setFormData] = useState({ client_id: '', due_date: '', status: 'draft', notes: '', first_reminder_days: 30, items: [{ name: '', description: '', quantity: 1, unit_price: 0 }] }); const [errors, setErrors] = useState({}); const [selectedItemIds, setSelectedItemIds] = useState(['custom']); // Track selected item ID for each item row const [timeInputModes, setTimeInputModes] = useState([false]); // Track if time input mode is enabled for each item const [timeInputValues, setTimeInputValues] = useState([{ hours: '', minutes: '' }]); // Track hours/minutes for each item // Convert hours and minutes to decimal quantity const convertTimeToQuantity = (hours, minutes) => { const h = parseFloat(hours) || 0; const m = parseFloat(minutes) || 0; const totalHours = h + (m / 60); return parseFloat(totalHours.toFixed(4)); }; const statusOptions = [ { value: 'draft', label: "Brouillon" }, { value: 'sent', label: "Envoyée" }, { value: 'paid', label: "Payée" }, { value: 'overdue', label: "En retard" }, { value: 'cancelled', label: "Annulée" } ]; const reminderOptions = [ { value: 30, label: '30 jours' }, { value: 14, label: '14 jours' }, { value: 7, label: '7 jours' }, { value: 3, label: '3 jours' } ]; useEffect(() => { loadData(); }, []); const loadData = async () => { try { setLoading(true); // Load clients const clientsResponse = await fetch('/zen/api/admin/clients?limit=1000', { credentials: 'include' }); const clientsData = await clientsResponse.json(); if (clientsData.success) { setClients(clientsData.clients || []); } else { toast.error("Échec du chargement des clients"); } // Load items (active only) const itemsResponse = await fetch('/zen/api/admin/items?limit=1000&is_active=true', { credentials: 'include' }); const itemsData = await itemsResponse.json(); if (itemsData.success) { setItems(itemsData.items || []); } } catch (error) { console.error('Error loading data:', error); toast.error("Échec du chargement des données"); } finally { setLoading(false); } }; const calculateTotals = () => { const subtotal = formData.items.reduce((sum, item) => { return sum + (parseFloat(item.quantity || 0) * parseFloat(item.unit_price || 0)); }, 0); const total = subtotal; return { subtotal: parseFloat(subtotal.toFixed(2)), total: parseFloat(total.toFixed(2)) }; }; // Calculate issue date based on due date and first reminder days const calculateIssueDate = (dueDate, reminderDays) => { if (!dueDate || !reminderDays) return ''; const issueDate = subtractDays(dueDate, parseInt(reminderDays)); return formatDateForInput(issueDate); }; const handleInputChange = (field, value) => { setFormData(prev => ({ ...prev, [field]: value })); if (errors[field]) { setErrors(prev => ({ ...prev, [field]: null })); } }; const handleItemChange = (index, field, value) => { const newItems = [...formData.items]; newItems[index] = { ...newItems[index], [field]: value }; setFormData(prev => ({ ...prev, items: newItems })); if (errors[`item_${index}_${field}`]) { setErrors(prev => ({ ...prev, [`item_${index}_${field}`]: null })); } }; // Toggle between quantity and time input mode const toggleTimeInputMode = (index) => { const newModes = [...timeInputModes]; newModes[index] = !newModes[index]; setTimeInputModes(newModes); // If switching to time mode, initialize with current quantity converted to time if (newModes[index]) { const currentQty = parseFloat(formData.items[index].quantity) || 0; const hours = Math.floor(currentQty); const minutes = Math.round((currentQty - hours) * 60); const newTimeValues = [...timeInputValues]; newTimeValues[index] = { hours: hours || '', minutes: minutes || '' }; setTimeInputValues(newTimeValues); } }; // Handle time input changes (hours or minutes) const handleTimeInputChange = (index, field, value) => { const newTimeValues = [...timeInputValues]; newTimeValues[index] = { ...newTimeValues[index], [field]: value }; setTimeInputValues(newTimeValues); // Convert to quantity and update the item const hours = field === 'hours' ? value : newTimeValues[index].hours; const minutes = field === 'minutes' ? value : newTimeValues[index].minutes; const quantity = convertTimeToQuantity(hours, minutes); handleItemChange(index, 'quantity', quantity); }; const handleSelectItem = (index, itemId) => { // Update the selected item ID for this row const newSelectedItemIds = [...selectedItemIds]; newSelectedItemIds[index] = itemId; setSelectedItemIds(newSelectedItemIds); if (itemId === '' || itemId === 'custom') { // Reset to custom/empty handleItemChange(index, 'name', ''); handleItemChange(index, 'description', ''); handleItemChange(index, 'unit_price', 0); return; } const selectedItem = items.find(item => item.id === parseInt(itemId)); if (selectedItem) { // Build full item name with category hierarchy let fullItemName = ''; if (selectedItem.parent_category_title) { fullItemName = `${selectedItem.parent_category_title} - ${selectedItem.category_title} - ${selectedItem.name}`; } else if (selectedItem.category_title) { fullItemName = `${selectedItem.category_title} - ${selectedItem.name}`; } else { fullItemName = selectedItem.name; } // Pre-fill fields from selected item const newItems = [...formData.items]; newItems[index] = { ...newItems[index], name: fullItemName, description: selectedItem.description || '', unit_price: selectedItem.unit_price }; setFormData(prev => ({ ...prev, items: newItems })); } }; const addItem = () => { setFormData(prev => ({ ...prev, items: [...prev.items, { name: '', description: '', quantity: 1, unit_price: 0 }] })); setSelectedItemIds(prev => [...prev, 'custom']); setTimeInputModes(prev => [...prev, false]); setTimeInputValues(prev => [...prev, { hours: '', minutes: '' }]); }; const removeItem = (index) => { if (formData.items.length > 1) { setFormData(prev => ({ ...prev, items: prev.items.filter((_, i) => i !== index) })); setSelectedItemIds(prev => prev.filter((_, i) => i !== index)); setTimeInputModes(prev => prev.filter((_, i) => i !== index)); setTimeInputValues(prev => prev.filter((_, i) => i !== index)); } }; const validateForm = () => { const newErrors = {}; if (!formData.client_id) { newErrors.client_id = "Le client est requis"; } if (!formData.due_date) { newErrors.due_date = "La date d'échéance est requise"; } formData.items.forEach((item, index) => { if (!item.name.trim()) { newErrors[`item_${index}_name`] = "Le nom de l'article est requis"; } if (!item.quantity || item.quantity <= 0) { newErrors[`item_${index}_quantity`] = "La quantité doit être supérieure à 0"; } if (item.unit_price < 0) { newErrors[`item_${index}_unit_price`] = "Le prix unitaire ne peut pas être négatif"; } }); setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async (e) => { e.preventDefault(); if (!validateForm()) { return; } try { setSaving(true); // Calculate issue_date before submitting const issue_date = calculateIssueDate(formData.due_date, formData.first_reminder_days); const invoiceDataToSubmit = { ...formData, issue_date }; const response = await fetch('/zen/api/admin/invoices', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ invoice: invoiceDataToSubmit }), }); const data = await response.json(); if (data.success) { toast.success("Facture créée avec succès"); router.push('/admin/invoice/invoices'); } else { toast.error(data.error || "Échec de la création de la facture"); } } catch (error) { console.error('Error creating invoice:', error); toast.error("Échec de la création de la facture"); } finally { setSaving(false); } }; const totals = calculateTotals(); return (
Remplissez les détails pour créer une nouvelle facture