diff --git a/src/components/Breadcrumbs.jsx b/src/components/Breadcrumbs.jsx new file mode 100644 index 0000000..8045b01 --- /dev/null +++ b/src/components/Breadcrumbs.jsx @@ -0,0 +1,64 @@ + +import React from 'react'; +import { useLocation, Link } from 'react-router-dom'; +import { ChevronRight, Home } from 'lucide-react'; + +const routeNameMap = { + 'crm': 'CRM', + 'sales': 'Ventes', + 'purchases': 'Achats', + 'support': 'SAV', + 'admin': 'Administration', + 'prospects': 'Prospects', + 'clients': 'Clients', + 'suppliers': 'Fournisseurs', + 'opportunities': 'Opportunités', + 'quotes': 'Devis', + 'orders': 'Commandes', + 'delivery-notes': 'Bons de livraison', + 'invoices': 'Factures', + 'credit-notes': 'Avoirs', + 'tickets': 'Tickets', + 'dashboard': 'Tableau de bord', + 'users': 'Utilisateurs', + 'settings': 'Paramètres', + 'profile': 'Profil', + 'preferences': 'Préférences' +}; + +const Breadcrumbs = () => { + const location = useLocation(); + const pathnames = location.pathname.split('/').filter((x) => x); + + if (pathnames.length === 0) return null; + + return ( + + ); +}; + +export default Breadcrumbs; diff --git a/src/components/CallToAction.jsx b/src/components/CallToAction.jsx new file mode 100644 index 0000000..d2203da --- /dev/null +++ b/src/components/CallToAction.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { motion } from 'framer-motion'; + +const CallToAction = () => { + return ( + + Let's turn your ideas into reality + + ); +}; + +export default CallToAction; diff --git a/src/components/ChartCard.tsx b/src/components/ChartCard.tsx new file mode 100644 index 0000000..fad52ee --- /dev/null +++ b/src/components/ChartCard.tsx @@ -0,0 +1,338 @@ +import React from 'react'; +import { + LineChart, Line, BarChart, Bar, PieChart, Pie, Cell, AreaChart, Area, + ResponsiveContainer, XAxis, YAxis, CartesianGrid, Tooltip, Legend, + RadialBarChart, RadialBar, LabelList +} from 'recharts'; +import { motion } from 'framer-motion'; +import { Maximize2 } from 'lucide-react'; + +const COLORS = ['#007E45', '#10b981', '#059669', '#047857', '#065f46', '#064e3b', '#022c22']; + +// ============================================ +// FORMATTERS +// ============================================ + +const formatToK = (value: number): string => { + if (value >= 1000000) { + return `${(value / 1000000).toFixed(1)}M€`; + } + if (value >= 1000) { + return `${(value / 1000).toFixed(0)}K€`; + } + return `${value}€`; +}; + +const formatToKShort = (value: number): string => { + if (value >= 1000000) { + return `${(value / 1000000).toFixed(1)}M`; + } + if (value >= 1000) { + return `${(value / 1000).toFixed(0)}K`; + } + return `${value}`; +}; + +const formatCurrency = (value: number): string => { + return new Intl.NumberFormat('fr-FR', { + style: 'currency', + currency: 'EUR', + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(value); +}; + +// ============================================ +// CUSTOM TOOLTIP +// ============================================ + +interface TooltipProps { + active?: boolean; + payload?: any[]; + label?: string; +} + +const CustomTooltip: React.FC = ({ active, payload, label }) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+ {payload.map((entry, index) => ( +
+
+ {entry.name}: + + {formatCurrency(entry.value)} + +
+ ))} +
+ ); + } + return null; +}; + +// ============================================ +// CUSTOM LABEL POUR LES VALEURS SUR LE GRAPHIQUE +// ============================================ + +const CustomLabel = (props: any) => { + const { x, y, value, index } = props; + return ( + + {formatToK(value)} + + ); +}; + +// ============================================ +// TYPES (exportés pour réutilisation) +// ============================================ + +interface ChartCardProps { + title: string; + type?: string; + data: any[]; + height?: number; + className?: string; + showLabels?: boolean; // Afficher les valeurs sur le graphique + showTotal?: boolean; // Afficher le total sous le titre + showYAxisLabel?: boolean; // Afficher le label de l'axe Y + yAxisLabel?: string; // Label de l'axe Y (ex: "Montant en K€") +} + +// ============================================ +// COMPOSANT PRINCIPAL +// ============================================ + +const ChartCard: React.FC = ({ + title, + type = 'line', + data, + height = 300, + className, + showLabels = true, + showTotal = true, + showYAxisLabel = true, + yAxisLabel = "Montant en K€" +}) => { + // Guard clause + if (!data || data.length === 0) { + return ( +
+

{title}

+
+ Pas de données +
+
+ ); + } + + // Calculer le total pour l'afficher + const total = data.reduce((sum, item) => sum + (item.value || 0), 0) / 1000 + + return ( + +
+
+

{title}

+ {showTotal && ( +

+ Total: {total.toFixed(0)} K € +

+ )} +
+ +
+ + {/* Label de l'axe Y */} + {showYAxisLabel && (type === 'line' || type === 'area' || type === 'bar') && ( +

{yAxisLabel}

+ )} + +
+ + {type === 'line' && ( + + + + + } /> + + {showLabels && ( + + )} + + + )} + + {type === 'area' && ( + + + + + + + + + + + } /> + + {showLabels && ( + + )} + + + )} + + {type === 'bar' && ( + + + + + } /> + + {data.map((entry, index) => ( + + ))} + {showLabels && ( + + )} + + + )} + + {type === 'donut' && ( + + + {data.map((entry, index) => ( + + ))} + + } /> + + + )} + + {type === 'radial' && ( + + + + + )} + +
+
+ ); +}; + +export default ChartCard; diff --git a/src/components/DataTable.jsx b/src/components/DataTable.jsx new file mode 100644 index 0000000..6c2d345 --- /dev/null +++ b/src/components/DataTable.jsx @@ -0,0 +1,155 @@ + +import React, { useState } from 'react'; +import { ChevronDown, ChevronUp, Search } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import Pagination from '@/components/Pagination'; +import CircularProgress from '@mui/material/CircularProgress'; + + + +const DataTable = ({ columns, data, onRowClick, actions, status = false, searchLabel = "Rechercher..." }) => { + const [sortColumn, setSortColumn] = useState(null); + const [sortDirection, setSortDirection] = useState('asc'); + const [searchTerm, setSearchTerm] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 10; + + const handleSort = (column) => { + if (sortColumn === column) { + setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); + } else { + setSortColumn(column); + setSortDirection('asc'); + } + }; + + + const filteredData = data.filter(row => + Object.values(row).some(value => + String(value).toLowerCase().includes(searchTerm.toLowerCase()) + ) + ); + + const sortedData = [...filteredData].sort((a, b) => { + if (!sortColumn) return 0; + const aVal = a[sortColumn]; + const bVal = b[sortColumn]; + if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; + if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; + return 0; + }); + + const paginatedData = sortedData.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage + ); + + const totalPages = Math.ceil(filteredData.length / itemsPerPage); + + return ( +
+
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2.5 bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-[#007E45] text-gray-900 dark:text-white" + /> +
+
+ +
+
+ + + + {columns.map((column) => ( + + ))} + {actions && } + + + + {status ? ( + // LOADING + + + + ) : paginatedData.length === 0 ? ( + // AUCUNE DONNÉE + + + + ) : ( + // DONNÉES + paginatedData.map((row, index) => ( + onRowClick?.(row)} + className="hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors cursor-pointer" + > + {columns.map((column) => ( + + ))} + + {actions && ( + + )} + + )) + )} + + +
column.sortable && handleSort(column.key)} + className={cn( + "px-6 py-3 text-left text-xs font-bold text-black dark:text-gray-400 uppercase tracking-wider", + column.sortable && "cursor-pointer hover:text-gray-900 dark:hover:text-white" + )} + > +
+ {column.label} + {column.sortable && sortColumn === column.key && ( + sortDirection === 'asc' ? : + )} +
+
Actions
+
+ +
+
+ Empty +
+ {column.render + ? column.render(row[column.key], row) + : row[column.key]} + +
e.stopPropagation()} + > + {actions(row)} +
+
+
+
+ + {totalPages > 1 && ( + + )} +
+ ); +}; + +export default DataTable; diff --git a/src/components/DropdownMenu.tsx b/src/components/DropdownMenu.tsx new file mode 100644 index 0000000..959792f --- /dev/null +++ b/src/components/DropdownMenu.tsx @@ -0,0 +1,159 @@ +import { useState, useEffect, useRef } from 'react'; +import { Edit, Download, FileSignature, MoreVertical, Trash2, CircleDot, Copy } from 'lucide-react'; + +export const DropdownMenuTable = ({ + row, + onEdit, + onStatus, + onESign, + onDownload, + onDelete, + onDulipcate +}: { + row: any; + onEdit?: () => void; + onStatus?: () => void; + onESign?: () => void; + onDownload?: () => void; + onDelete?: () => void; + onDulipcate?: () => void; +}) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + // Fermer le menu quand on clique à l'extérieur + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + return ( +
+ + + {isOpen && ( +
+ + + { + onEdit && ( + + ) + } + + { + onDulipcate && ( + + ) + } + + { + onStatus && ( + + ) + } + + + { + onESign && ( + + ) + } + + { + onDownload && ( + + ) + } + + + { + onDelete && ( + <> +
+ + + + ) + } + + + +
+ )} +
+ ); +}; diff --git a/src/components/FormModal.jsx b/src/components/FormModal.jsx new file mode 100644 index 0000000..e639f32 --- /dev/null +++ b/src/components/FormModal.jsx @@ -0,0 +1,146 @@ + +import React from 'react'; +import { X } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { cn } from '@/lib/utils'; + +export const FormSection = ({ title, description, children, className }) => ( +
+

{title}

+ {description &&

{description}

} +
+ {children} +
+
+); + + +export const FormField = ({ label, children, required, error, fullWidth, className }) => ( +
+ + {children} + {error &&

{error}

} +
+); + +export const Input = React.forwardRef(({ className, error, ...props }, ref) => ( + +)); +Input.displayName = "Input"; + +export const Select = React.forwardRef(({ className, error, children, ...props }, ref) => ( + +)); +Select.displayName = "Select"; + +export const Textarea = React.forwardRef(({ className, error, ...props }, ref) => ( +