|
|
| import { useState, useRef, useEffect } from "react";
|
| import { Send, Plus, ArrowRight, AlertTriangle } from "lucide-react";
|
| import { Input } from "@/components/ui/input";
|
| import { Button } from "@/components/ui/button";
|
| import { ScrollArea } from "@/components/ui/scroll-area";
|
| import { Avatar } from "@/components/ui/avatar";
|
| import { cn } from "@/lib/utils";
|
| import { motion } from "framer-motion";
|
| import { toast } from "sonner";
|
| import { useNavigate } from "react-router-dom";
|
| import database from "@/services/database";
|
|
|
| type Message = {
|
| id: number;
|
| type: 'user' | 'bot';
|
| text: string;
|
| timestamp: string;
|
| };
|
|
|
| type TransactionData = {
|
| amount?: number;
|
| category?: string;
|
| description?: string;
|
| date?: string;
|
| type?: 'income' | 'expense';
|
| quantity?: number;
|
| product?: string;
|
| };
|
|
|
| const AITransactionChat = () => {
|
| const [messages, setMessages] = useState<Message[]>([
|
| {
|
| id: 1,
|
| type: 'bot',
|
| text: "Bienvenue! Je suis votre assistant de gestion. Vous pouvez me parler de vos transactions ou me demander des rapports sur vos stocks. Par exemple: 'Vendu 5 sacs de riz à 2000 FCFA chacun' ou 'Quels produits sont en rupture de stock?'",
|
| timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
|
| }
|
| ]);
|
| const [input, setInput] = useState('');
|
| const [isProcessing, setIsProcessing] = useState(false);
|
| const [extractedData, setExtractedData] = useState<TransactionData | null>(null);
|
| const scrollAreaRef = useRef<HTMLDivElement>(null);
|
| const navigate = useNavigate();
|
|
|
|
|
| useEffect(() => {
|
| if (scrollAreaRef.current) {
|
| const scrollContainer = scrollAreaRef.current.querySelector('[data-radix-scroll-area-viewport]');
|
| if (scrollContainer) {
|
| scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
| }
|
| }
|
| }, [messages]);
|
|
|
| const handleSendMessage = () => {
|
| if (!input.trim()) return;
|
|
|
| const userMessage = {
|
| id: messages.length + 1,
|
| type: 'user' as const,
|
| text: input,
|
| timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
|
| };
|
|
|
| setMessages(prev => [...prev, userMessage]);
|
| setInput('');
|
| setIsProcessing(true);
|
|
|
|
|
| setTimeout(() => {
|
| processUserInput(userMessage.text);
|
| }, 1000);
|
| };
|
|
|
| const processUserInput = (text: string) => {
|
|
|
|
|
|
|
| const lowerText = text.toLowerCase();
|
|
|
|
|
| if (
|
| lowerText.includes('stock') ||
|
| lowerText.includes('inventaire') ||
|
| lowerText.includes('rupture') ||
|
| lowerText.includes('rapport') ||
|
| lowerText.includes('report')
|
| ) {
|
| handleStockQuery(text);
|
| return;
|
| }
|
|
|
|
|
| const moneyRegex = /(\d+(?:\.\d{1,2})?)\s*(?:fcfa|cfa|\$|€)?/i;
|
| const moneyMatch = lowerText.match(moneyRegex);
|
| const amount = moneyMatch ? parseFloat(moneyMatch[1]) : undefined;
|
|
|
| const quantityRegex = /(\d+)\s*(sac|sachet|boîte|kg|pièce|piece|carton|bouteille|paquet|article)/i;
|
| const quantityMatch = lowerText.match(quantityRegex);
|
| const quantity = quantityMatch ? parseInt(quantityMatch[1]) : 1;
|
|
|
| const productRegex = /(riz|lait|sucre|huile|farine|café|cafe|thé|the|eau|savon|pâtes|pates|tomate|oignon|poisson|viande|poulet|œuf|oeuf)/i;
|
| const productMatch = lowerText.match(productRegex);
|
| const product = productMatch ? productMatch[1] : undefined;
|
|
|
| const isSale =
|
| lowerText.includes('vend') ||
|
| lowerText.includes('vendu') ||
|
| lowerText.includes('vendr') ||
|
| lowerText.includes('sold') ||
|
| lowerText.includes('sale');
|
|
|
| const isPurchase =
|
| lowerText.includes('achet') ||
|
| lowerText.includes('acheter') ||
|
| lowerText.includes('acheté') ||
|
| lowerText.includes('bought') ||
|
| lowerText.includes('purchas');
|
|
|
| const data: TransactionData = {
|
| amount: amount,
|
| type: isSale ? 'income' : isPurchase ? 'expense' : undefined,
|
| date: new Date().toISOString().split('T')[0],
|
| product: product,
|
| quantity: quantity,
|
| description: product ? `${isSale ? 'Vente' : 'Achat'} de ${product}` : text.substring(0, 50),
|
| category: product ? 'Marchandise' : 'Autre'
|
| };
|
|
|
| setExtractedData(data);
|
|
|
|
|
| let response: string;
|
|
|
| if (amount && (isSale || isPurchase) && product) {
|
| const action = isSale ? 'vente' : 'achat';
|
| response = `J'ai compris que vous avez ${isSale ? 'vendu' : 'acheté'} `;
|
|
|
| if (quantity > 1) {
|
| response += `${quantity} unités de ${product}`;
|
| } else {
|
| response += `du ${product}`;
|
| }
|
|
|
| if (amount) {
|
| response += ` pour ${amount} FCFA`;
|
| }
|
|
|
| response += `. Voulez-vous enregistrer cette ${action} ?`;
|
| } else if (amount) {
|
| response = `J'ai détecté un montant de ${amount} FCFA. Pouvez-vous préciser s'il s'agit d'une vente ou d'un achat, et pour quel produit ?`;
|
| } else if (product) {
|
| response = `J'ai identifié le produit: ${product}. Pouvez-vous préciser la quantité et le montant de la transaction ?`;
|
| } else {
|
| response = `Je n'ai pas bien compris votre transaction. Pouvez-vous préciser le produit, la quantité et le montant ? Par exemple: "Vendu 5 sacs de riz à 2000 FCFA chacun"`;
|
| }
|
|
|
| const botMessage = {
|
| id: messages.length + 2,
|
| type: 'bot' as const,
|
| text: response,
|
| timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
|
| };
|
|
|
| setMessages(prev => [...prev, botMessage]);
|
| setIsProcessing(false);
|
| };
|
|
|
| const handleStockQuery = (text: string) => {
|
| const lowerText = text.toLowerCase();
|
| let response: string;
|
|
|
|
|
| if (
|
| lowerText.includes('rupture') ||
|
| lowerText.includes('faible') ||
|
| lowerText.includes('low') ||
|
| lowerText.includes('alerte')
|
| ) {
|
| response = "D'après mon analyse, les produits suivants sont en stock faible : Riz (5 sacs), Lait (3 cartons), Huile (2 bidons). Je recommande de vous réapprovisionner bientôt.";
|
| }
|
|
|
| else if (
|
| lowerText.includes('rapport') ||
|
| lowerText.includes('report') ||
|
| lowerText.includes('ventes') ||
|
| lowerText.includes('sales')
|
| ) {
|
| response = "Voici un résumé de vos ventes: Cette semaine, vous avez vendu pour 125,000 FCFA de marchandises, soit une augmentation de 12% par rapport à la semaine précédente. Vos produits les plus vendus sont le riz (45%), le lait (30%) et l'huile (15%).";
|
| }
|
|
|
| else if (
|
| lowerText.includes('prix') ||
|
| lowerText.includes('price') ||
|
| lowerText.includes('tarif') ||
|
| lowerText.includes('recommand')
|
| ) {
|
| response = "Basé sur les tendances actuelles du marché, je vous recommande d'augmenter le prix du riz de 5% (de 10,000 à 10,500 FCFA par sac) et de maintenir le prix des autres produits. Il y a une forte demande pour le riz cette semaine.";
|
| }
|
|
|
| else {
|
| response = "Votre inventaire actuel comprend: Riz (25 sacs), Lait (18 cartons), Sucre (30 kg), Huile (12 bidons), Savon (40 pièces). Voulez-vous des informations sur un produit spécifique ?";
|
| }
|
|
|
| const botMessage = {
|
| id: messages.length + 2,
|
| type: 'bot' as const,
|
| text: response,
|
| timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
|
| };
|
|
|
| setMessages(prev => [...prev, botMessage]);
|
| setIsProcessing(false);
|
|
|
|
|
| if (lowerText.includes('rapport') || lowerText.includes('report')) {
|
| setTimeout(() => {
|
| const reportMessage = {
|
| id: messages.length + 3,
|
| type: 'bot' as const,
|
| text: "Voulez-vous voir des rapports plus détaillés? Je peux vous montrer la page des rapports.",
|
| timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
|
| };
|
|
|
| setMessages(prev => [...prev, reportMessage]);
|
| }, 1000);
|
| }
|
| };
|
|
|
| const formatDateFromTerm = (term: string): string => {
|
| const today = new Date();
|
|
|
| switch(term.toLowerCase()) {
|
| case 'yesterday':
|
| case 'hier':
|
| const yesterday = new Date(today);
|
| yesterday.setDate(yesterday.getDate() - 1);
|
| return yesterday.toISOString().split('T')[0];
|
| case 'last week':
|
| case 'semaine dernière':
|
| const lastWeek = new Date(today);
|
| lastWeek.setDate(lastWeek.getDate() - 7);
|
| return lastWeek.toISOString().split('T')[0];
|
| case 'this morning':
|
| case 'ce matin':
|
| case 'today':
|
| case 'aujourd\'hui':
|
| default:
|
| return today.toISOString().split('T')[0];
|
| }
|
| };
|
|
|
| const handleAddTransaction = () => {
|
| if (extractedData?.amount) {
|
| toast.success('Transaction ajoutée avec succès!');
|
|
|
|
|
| if (extractedData.type === 'income' && extractedData.product && extractedData.quantity && extractedData.quantity > 3) {
|
| setTimeout(() => {
|
| const alertMessage = {
|
| id: messages.length + 4,
|
| type: 'bot' as const,
|
| text: `⚠️ Alerte: Après cette vente, votre stock de ${extractedData.product} est faible. Il ne reste que ${Math.floor(Math.random() * 5) + 1} unités. Pensez à vous réapprovisionner.`,
|
| timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
|
| };
|
|
|
| setMessages(prev => [...prev, alertMessage]);
|
| }, 1500);
|
| }
|
|
|
| setTimeout(() => navigate('/transactions'), 2500);
|
| } else {
|
| toast.error('Veuillez fournir un montant valide');
|
| }
|
| };
|
|
|
| return (
|
| <div className="flex flex-col h-[calc(100vh-220px)] bg-white dark:bg-violet-darker rounded-lg border border-border overflow-hidden shadow-md">
|
| <div className="p-4 border-b border-border bg-muted/30 dark:bg-violet-dark/30">
|
| <h3 className="font-medium text-center">
|
| Assistant IA de Gestion
|
| </h3>
|
| </div>
|
|
|
| <ScrollArea className="flex-1 p-4" ref={scrollAreaRef}>
|
| <div className="space-y-4 pb-4">
|
| {messages.map((message) => (
|
| <motion.div
|
| key={message.id}
|
| initial={{ opacity: 0, y: 10 }}
|
| animate={{ opacity: 1, y: 0 }}
|
| transition={{ duration: 0.2 }}
|
| className={cn(
|
| "flex",
|
| message.type === "user" ? "justify-end" : "justify-start"
|
| )}
|
| >
|
| {message.type === "bot" && (
|
| <Avatar className="h-8 w-8 mr-2 bg-primary text-white">
|
| <span>AI</span>
|
| </Avatar>
|
| )}
|
|
|
| <div
|
| className={cn(
|
| "max-w-[80%] p-3 rounded-xl",
|
| message.type === "user"
|
| ? "bg-primary text-primary-foreground rounded-tr-none"
|
| : "bg-muted dark:bg-violet-muted/30 rounded-tl-none"
|
| )}
|
| >
|
| <p className="text-sm">{message.text}</p>
|
| <span className={cn(
|
| "text-xs mt-1 block opacity-70",
|
| message.type === "user" ? "text-right" : ""
|
| )}>
|
| {message.timestamp}
|
| </span>
|
| </div>
|
| </motion.div>
|
| ))}
|
|
|
| {isProcessing && (
|
| <div className="flex justify-start">
|
| <Avatar className="h-8 w-8 mr-2 bg-primary text-white">
|
| <span>AI</span>
|
| </Avatar>
|
| <div className="bg-muted dark:bg-violet-muted/30 p-3 rounded-xl rounded-tl-none">
|
| <span className="flex items-center space-x-1">
|
| <span className="w-2 h-2 bg-primary dark:bg-violet rounded-full animate-bounce" style={{animationDelay: '0ms'}}></span>
|
| <span className="w-2 h-2 bg-primary dark:bg-violet rounded-full animate-bounce" style={{animationDelay: '150ms'}}></span>
|
| <span className="w-2 h-2 bg-primary dark:bg-violet rounded-full animate-bounce" style={{animationDelay: '300ms'}}></span>
|
| </span>
|
| </div>
|
| </div>
|
| )}
|
| </div>
|
| </ScrollArea>
|
|
|
| {extractedData?.amount && (
|
| <motion.div
|
| initial={{ opacity: 0, height: 0 }}
|
| animate={{ opacity: 1, height: 'auto' }}
|
| className="p-4 border-t border-border bg-muted/30 dark:bg-violet-dark/10"
|
| >
|
| <div className="flex items-center justify-between">
|
| <div>
|
| <p className="text-sm font-medium">Transaction prête à ajouter:</p>
|
| <p className="text-sm">
|
| {extractedData.amount} FCFA - {extractedData.product || extractedData.category || 'Non catégorisé'} ({extractedData.type === 'income' ? 'Vente' : 'Achat'})
|
| {extractedData.quantity && extractedData.quantity > 1 ? ` × ${extractedData.quantity}` : ''}
|
| </p>
|
| </div>
|
| <Button size="sm" onClick={handleAddTransaction} className="gap-1">
|
| <Plus size={16} /> Ajouter <ArrowRight size={14} />
|
| </Button>
|
| </div>
|
| </motion.div>
|
| )}
|
|
|
| <div className="p-3 border-t border-border flex items-center gap-2 bg-card/50 dark:bg-violet-darker">
|
| <Input
|
| placeholder="Décrivez votre transaction ou demandez des informations..."
|
| value={input}
|
| onChange={(e) => setInput(e.target.value)}
|
| onKeyDown={(e) => {
|
| if (e.key === "Enter") {
|
| handleSendMessage();
|
| }
|
| }}
|
| className="flex-1 bg-background dark:bg-violet-darker border-slate-200 dark:border-violet-muted/30"
|
| />
|
| <Button
|
| size="icon"
|
| className="bg-primary hover:bg-primary/90 dark:bg-violet dark:hover:bg-violet-dark"
|
| onClick={handleSendMessage}
|
| >
|
| <Send size={18} />
|
| </Button>
|
| </div>
|
| </div>
|
| );
|
| };
|
|
|
| export default AITransactionChat;
|
|
|