push all page

This commit is contained in:
mickael 2026-01-20 11:06:52 +03:00
parent 13a4b3cd8a
commit fa25518e6b
116 changed files with 8896 additions and 1 deletions

1
.gitignore vendored
View file

@ -27,4 +27,3 @@ dist-ssr
.env .env
.env.staging .env.staging
.env.production .env.production
src/

44
src/App.jsx Normal file
View file

@ -0,0 +1,44 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { Toaster } from '@/components/ui/toaster';
import { ThemeProvider } from '@/contexts/ThemeContext';
import DatavenRoute from '@/routes/DatavenRoute';
import ProtectedRoute from '@/routes/ProtectedRoute';
import Login from '@/pages/auth/Login';
import Forgot from '@/pages/auth/forgot';
import Reset from '@/pages/auth/reset';
import { SignatureProvider } from './contexts/SignatureContext';
function App() {
return (
<ThemeProvider>
<SignatureProvider>
<Router>
<Routes>
<Route path="/" element={<Login />} />
<Route path="/forgot" element={<Forgot />} />
<Route path="/reset" element={<Reset />} />
<Route
path="/home/*"
element={
<ProtectedRoute>
<DatavenRoute />
</ProtectedRoute>
}
/>
<Route path="*" element={<Login />} />
</Routes>
<Toaster toastOptions={{ className: "z-[9999]" }} />
</Router>
</SignatureProvider>
</ThemeProvider>
);
}
export default App;

135
src/config/apiConfig.ts Normal file
View file

@ -0,0 +1,135 @@
import { pdfInterface } from "@/types/sageTypes"
export const API_CONFIG = {
baseURL: import.meta.env.VITE_API_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
}
export const API_ENDPOINTS = {
// ========= Authentification ==========
auth: '/auth/me',
login: '/auth/login',
logout: '/auth/logout',
refreshToken: '/auth/refresh',
forgotPassword: '/auth/forgot-password',
resetPassword: '/auth/reset-password',
// entreprise
searchEntreprise : (query: string) => `/entreprises/search?q=${query}`,
// client
clients: '/clients',
clientById: (numero: string) => `/clients/${numero}`,
// societe
societe: '/societe/info',
// gateways
gateways: '/sage-gateways',
gatewaysById: (id: string) => `/sage-gateways/${id}`,
// contacts
contactsClient: (numero: string) => `/tiers/${numero}/contacts`,
contactId: (numero: string, contact_numero: number) => `/tiers/${numero}/contacts/${contact_numero}`,
// commercial
commercials: '/collaborateurs',
commercialsById: (numero: string) => `/collaborateurs/${numero}`,
// Articles
articles: '/articles',
articleById: (reference: string) => `/articles/${reference}`,
// stock
addStock: '/stock/entree',
// devis
devis: '/devis',
devisById: (id: string) => `/devis/${id}`,
devisPDF: (id: string) => `/devis/${id}/pdf`,
devisEnvoyer: (id: string) => `/devis/${id}/envoyer`,
devisSigner: (id: string) => `/devis/${id}/signer`,
devisStatut: (id: string) => `/devis/${id}/statut`,
devisContact: (id: string) => `/devis/${id}/contact`,
// fournisseur
fournisseurs: '/fournisseurs',
fournisseursById: (numero: string) => `/fournisseurs/${numero}`,
// famille
familles: '/familles',
familleId: (code: string) => `/familles/${code}`,
// uniersign
downloadSignedDocument: (transactionId: string) => `/universign/transactions/${transactionId}/document/download`,
getTransaction: (transactionId: string) => `/universign/transactions/${transactionId}`,
// Bon de Livraison
bl: '/livraisons',
blById: (id: string) => `/livraisons/${id}`,
// client
avoirs: '/avoirs',
avoirsById: (numero: string) => `/avoirs/${numero}`,
// reglements
reglement: "/reglements",
reglementById: (id: string) => `/reglements/facture/${id}`,
reglerFacture : '/reglements/multiple',
mode: '/reglements/modes',
devises: '/devises',
tresorerie: '/journaux/tresorerie',
compte_generaux: '/comptes-generaux',
taux: '/tva/taux',
encaissement : '/parametres/encaissement',
// US-A2 - Workflow
devisToFacture: (id: string) => `/workflow/devis/${id}/to-facture`,
devisToCommande: (id: string) => `/workflow/devis/${id}/to-commande`,
commandeToBL: (id: string) => `/workflow/commande/${id}/to-livraison`,
commandeToFacture: (id: string) => `/workflow/commande/${id}/to-facture`,
blToFacture: (id: string) => `/workflow/livraison/${id}/to-facture`,
commandes: '/commandes',
commandeById: (id: string) => `/commandes/${id}`,
// factures
factures: '/factures',
factureValider: (numero_facture: string) => `/factures/${numero_facture}/valider`,
factureById: (id: string) => `/factures/${id}`,
// US-A3 - Signature
signatureSend: '/signature/universign/send',
signatureStatus: '/signature/universign/status',
// US-A4/A8 - Emails
emailBatch: '/emails/send-batch',
emailLogs: '/emails/logs',
emailLogsExport: '/emails/logs/export',
// US-A5 - Remises
devisValiderRemise: '/devis/valider-remise',
// US-A6 - Relance Devis
devisRelancerSignature: (id: string) => `/devis/${id}/relancer-signature`,
// US-A7 - Relance Facture
factureRelancer: (id: string) => `/factures/${id}/relancer`,
// US-A10 - Templates
templates: '/templates/emails',
templateById: (id: string) => `/templates/emails/${id}`,
templatePreview: '/templates/emails/preview',
// Admin
cacheInfo: '/admin/cache/info',
cacheRefresh: '/admin/cache/refresh',
queueStatus: '/admin/queue/status',
health: '/health',
}

132
src/config/moduleConfig.js Normal file
View file

@ -0,0 +1,132 @@
import {
LayoutDashboard,
Users,
UserPlus,
ShoppingCart,
FileText,
CreditCard,
Settings,
PieChart,
Wallet,
Briefcase,
FileBarChart,
CheckSquare,
Upload,
Package,
Truck,
FolderOpen,
PenTool,
History,
BarChart3,
Scale,
BookOpen,
ScrollText,
Tags,
Building2,
Calculator,
ListTodo,
ShieldCheck,
PanelLeft
} from 'lucide-react';
export const MODULE_GESTION = 'gestion';
export const MODULE_COMPTABILITE = 'compta';
export const getMenuForModule = (currentModule) => {
if (currentModule === MODULE_COMPTABILITE) {
return [
{
title: "Pilotage",
items: [
{ to: "/comptabilite/dashboard", icon: LayoutDashboard, label: "Tableau de bord" },
{ to: "/comptabilite/transactions", icon: Wallet, label: "Banque & Transactions" },
]
},
{
title: "Saisie & Journaux",
items: [
{ to: "/comptabilite/ecritures", icon: PenTool, label: "Saisie d'écritures" },
{ to: "/comptabilite/journaux", icon: BookOpen, label: "Journaux" },
{ to: "/comptabilite/rapprochement", icon: Scale, label: "Rapprochement" },
]
},
{
title: "États & Déclarations",
items: [
{ to: "/comptabilite/grand-livre", icon: ScrollText, label: "Grand Livre" },
{ to: "/comptabilite/balance-generale", icon: Scale, label: "Balance Générale" },
{ to: "/comptabilite/balance-clients", icon: Users, label: "Balance Clients" },
{ to: "/comptabilite/balance-fournisseurs", icon: Building2, label: "Balance Fournisseurs" },
{ to: "/comptabilite/tva", icon: FileText, label: "Déclaration TVA" },
]
},
{
title: "Paramètres",
items: [
{ to: "/comptabilite/plan-comptable", icon: Tags, label: "Plan Comptable" },
{ to: "/comptabilite/analytique", icon: PieChart, label: "Analytique" },
{ to: "/comptabilite/imports-exports", icon: Upload, label: "Imports / Exports" },
]
}
];
}
// Default to MODULE_GESTION
return [
{
title: "Pilotage",
items: [
{ to: "/home/", icon: LayoutDashboard, label: "Tableau de bord", end: true },
]
},
{
title: "Relations Tiers",
items: [
// { to: "/home/prospects", icon: UserPlus, label: "Prospects" },
{ to: "/home/clients", icon: Users, label: "Clients" },
{ to: "/home/fournisseurs", icon: Building2, label: "Fournisseurs" },
]
},
{
title: "Ventes",
items: [
{ to: "/home/devis", icon: FileText, label: "Devis" },
{ to: "/home/commandes", icon: ShoppingCart, label: "Commandes" },
{ to: "/home/bons-livraison", icon: Truck, label: "Bons de livraison" },
{ to: "/home/factures", icon: FileBarChart, label: "Factures" },
{ to: "/home/avoirs", icon: FileText, label: "Avoirs" },
// { to: "/home/relances", icon: History, label: "Relances" },
{ to: "/home/reglements", icon: CreditCard, label: "Règlements" },
]
},
{
title: "Catalogue",
items: [
{ to: "/home/articles", icon: Package, label: "Articles" },
{ to: "/home/familles-articles", icon: Tags, label: "Familles" },
]
},
{
title: "Vues personnalisées",
items: [
{ to: "/home/sage-builder", icon: PanelLeft, label: "Tableau des ventes" },
]
},
// {
// title: "Signature",
// items: [
// { to: "/home/signature/dashboard", icon: PenTool, label: "Tableau de bord" },
// { to: "/home/signature/tracking", icon: History, label: "Suivi" },
// { to: "/home/signature/purchase", icon: ShoppingCart, label: "Acheter des crédits" },
// ]
// },
// {
// title: "Administration",
// items: [
// { to: "/home/utilisateurs", icon: Users, label: "Utilisateurs" },
// { to: "/home/parametres", icon: Settings, label: "Paramètres" },
// ]
// }
];
};

View file

@ -0,0 +1,63 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
const BannerContext = createContext();
export const useBanners = () => useContext(BannerContext);
export const BannerProvider = ({ children }) => {
const [priorityBanner, setPriorityBanner] = useState({
visible: true,
message: "Plateforme agréée facture électronique 2026",
type: "info", // info, warning
dismissible: true,
id: "priority-2026-mandate"
});
const [secondaryBanner, setSecondaryBanner] = useState({
visible: true,
message: "Bienvenue sur la nouvelle version de Dataven. Découvrez les nouvelles fonctionnalités.",
dismissible: true,
id: "secondary-welcome-v3"
});
// Load dismissed state from localStorage
useEffect(() => {
const dismissedPriority = localStorage.getItem(`banner-dismissed-${priorityBanner.id}`);
const dismissedSecondary = localStorage.getItem(`banner-dismissed-${secondaryBanner.id}`);
if (dismissedPriority) {
setPriorityBanner(prev => ({ ...prev, visible: false }));
}
if (dismissedSecondary) {
setSecondaryBanner(prev => ({ ...prev, visible: false }));
}
}, []);
const dismissPriority = () => {
if (priorityBanner.dismissible) {
setPriorityBanner(prev => ({ ...prev, visible: false }));
localStorage.setItem(`banner-dismissed-${priorityBanner.id}`, 'true');
}
};
const dismissSecondary = () => {
if (secondaryBanner.dismissible) {
setSecondaryBanner(prev => ({ ...prev, visible: false }));
localStorage.setItem(`banner-dismissed-${secondaryBanner.id}`, 'true');
}
};
return (
<BannerContext.Provider value={{
priorityBanner,
secondaryBanner,
dismissPriority,
dismissSecondary,
setPriorityBanner,
setSecondaryBanner
}}>
{children}
</BannerContext.Provider>
);
};

View file

@ -0,0 +1,46 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
const CompanyContext = createContext();
export const useCompany = () => {
const context = useContext(CompanyContext);
if (!context) {
throw new Error('useCompany must be used within a CompanyProvider');
}
return context;
};
const AVAILABLE_COMPANIES = [
{ id: 'c1', name: 'Acme Corp' },
{ id: 'c2', name: 'Tech Solutions' },
{ id: 'c3', name: 'Global Services' }
];
export const CompanyProvider = ({ children }) => {
const [currentCompany, setCurrentCompany] = useState(() => {
const saved = localStorage.getItem('currentCompany');
return saved ? JSON.parse(saved) : AVAILABLE_COMPANIES[0];
});
useEffect(() => {
localStorage.setItem('currentCompany', JSON.stringify(currentCompany));
}, [currentCompany]);
const switchCompany = (companyId) => {
const company = AVAILABLE_COMPANIES.find(c => c.id === companyId);
if (company) {
setCurrentCompany(company);
}
};
return (
<CompanyContext.Provider value={{
currentCompany,
switchCompany,
availableCompanies: AVAILABLE_COMPANIES
}}>
{children}
</CompanyContext.Provider>
);
};

View file

@ -0,0 +1,43 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
const DisplayModeContext = createContext();
export const DisplayModeProvider = ({ children }) => {
const [displayMode, setDisplayMode] = useState(() => {
return localStorage.getItem('displayMode') || 'comfortable';
});
const [isPdfPreviewVisible, setIsPdfPreviewVisible] = useState(() => {
return localStorage.getItem('pdfPreviewVisible') === 'true';
});
useEffect(() => {
localStorage.setItem('displayMode', displayMode);
}, [displayMode]);
useEffect(() => {
localStorage.setItem('pdfPreviewVisible', isPdfPreviewVisible);
}, [isPdfPreviewVisible]);
const toggleDisplayMode = () => {
setDisplayMode(prev => prev === 'compact' ? 'comfortable' : 'compact');
};
const togglePdfPreview = () => {
setIsPdfPreviewVisible(prev => !prev);
};
return (
<DisplayModeContext.Provider value={{
displayMode,
toggleDisplayMode,
isPdfPreviewVisible,
togglePdfPreview,
setIsPdfPreviewVisible
}}>
{children}
</DisplayModeContext.Provider>
);
};
export const useDisplayMode = () => useContext(DisplayModeContext);

View file

@ -0,0 +1,59 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
const ModuleContext = createContext();
export const MODULE_GESTION = 'gestion';
export const MODULE_COMPTABILITE = 'comptabilite';
export const ModuleProvider = ({ children }) => {
// Initialize from localStorage or default to 'gestion'
const [currentModule, setCurrentModule] = useState(() => {
const saved = localStorage.getItem('dataven_current_module');
return saved || MODULE_GESTION;
});
const navigate = useNavigate();
const location = useLocation();
// Persist to localStorage whenever module changes
useEffect(() => {
localStorage.setItem('dataven_current_module', currentModule);
}, [currentModule]);
const changeModule = (newModule) => {
if (newModule === currentModule) return;
setCurrentModule(newModule);
// Smart Navigation Logic
if (newModule === MODULE_COMPTABILITE) {
// If switching TO Compta, go to Compta Dashboard unless already deep in a compta page (unlikely if we are in Gesco mode)
if (!location.pathname.startsWith('/comptabilite')) {
// navigate('/comptabilite/dashboard');
navigate('/home');
}
}
else if (newModule === MODULE_GESTION) {
// If switching TO Gesco, go to Main Dashboard if currently in a Compta page
if (location.pathname.startsWith('/comptabilite')) {
navigate('/home');
}
}
};
return (
<ModuleContext.Provider value={{ currentModule, changeModule }}>
{children}
</ModuleContext.Provider>
);
};
export const useModule = () => {
const context = useContext(ModuleContext);
if (!context) {
throw new Error('useModule must be used within a ModuleProvider');
}
return context;
};

View file

@ -0,0 +1,82 @@
import React, { createContext, useContext, useState, useCallback } from 'react';
const GlobalMessageContext = createContext(null);
export const GlobalMessageProvider = ({ children }) => {
const [messageState, setMessageState] = useState({
isVisible: false,
text: '',
type: 'info', // info, warning, success, error
dismissible: true,
scrolling: false,
icon: 'info',
actionLabel: null,
onAction: null,
});
const showMessage = useCallback((text, options = {}) => {
// Hard blocklist for legacy placeholders / prompt-related messaging.
// If a legacy message tries to render (toast-like banners, announcements), do nothing.
const msg = (text ?? '').toString().replace(/\s+/g, ' ').trim();
const BLOCKLIST = [
/prochain\s+demande/i,
/prochaine\s+demande/i,
/prochain\s+prompt/i,
/\bprompt\b/i,
/\bllm\b/i,
/\bopenai\b/i,
/\bgpt\b/i,
/\bclaude\b/i,
/pas\s+encore\s+impl[ée]ment/i,
/sera\s+impl[ée]ment/i,
/\bcoming\s+soon\b/i,
/\bnot\s+implemented\b/i,
];
if (msg && BLOCKLIST.some((re) => re.test(msg))) {
return;
}
setMessageState({
isVisible: true,
text,
type: options.type || 'info',
dismissible: options.dismissible !== undefined ? options.dismissible : true,
scrolling: options.scrolling || false,
icon: options.icon || 'info',
actionLabel: options.actionLabel || null,
onAction: options.onAction || null,
});
}, []);
const hideMessage = useCallback(() => {
setMessageState(prev => ({ ...prev, isVisible: false }));
}, []);
const clearMessage = useCallback(() => {
setMessageState({
isVisible: false,
text: '',
type: 'info',
dismissible: true,
scrolling: false,
icon: 'info',
actionLabel: null,
onAction: null,
});
}, []);
return (
<GlobalMessageContext.Provider value={{ messageState, showMessage, hideMessage, clearMessage }}>
{children}
</GlobalMessageContext.Provider>
);
};
export const useGlobalMessage = () => {
const context = useContext(GlobalMessageContext);
if (!context) {
throw new Error('useGlobalMessage must be used within a GlobalMessageProvider');
}
return context;
};

View file

@ -0,0 +1,86 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { toast } from '@/components/ui/use-toast';
import { universignService } from '@/service/signature/universignService';
const SignatureContext = createContext();
export const useSignature = () => {
const context = useContext(SignatureContext);
if (!context) {
throw new Error('useSignature must be used within a SignatureProvider');
}
return context;
};
export const SignatureProvider = ({ children }) => {
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState(null);
const [signatures, setSignatures] = useState([]);
const [history, setHistory] = useState([]);
const [refreshTrigger, setRefreshTrigger] = useState(0);
// Initial load
useEffect(() => {
const loadData = async () => {
setLoading(true);
try {
const [statsData, sigsData, historyData] = await Promise.all([
universignService.getStats(),
universignService.getSignatures(),
universignService.getConsumptionHistory()
]);
setStats(statsData);
setSignatures(sigsData);
setHistory(historyData);
} catch (error) {
console.error("Error loading signature data:", error);
toast({
title: "Erreur",
description: "Impossible de charger les données de signature.",
variant: "destructive"
});
} finally {
setLoading(false);
}
};
loadData();
}, [refreshTrigger]);
const refreshData = () => setRefreshTrigger(prev => prev + 1);
const purchaseCredits = async (amount) => {
try {
await universignService.purchaseCredits(amount);
refreshData();
return true;
} catch (error) {
return false;
}
};
const sendReminder = async (signatureId) => {
try {
await universignService.sendReminder(signatureId);
refreshData();
return true;
} catch (error) {
return false;
}
};
return (
<SignatureContext.Provider value={{
loading,
stats,
signatures,
history,
refreshData,
purchaseCredits,
sendReminder
}}>
{children}
</SignatureContext.Provider>
);
};

View file

@ -0,0 +1,49 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
// Création du contexte
const ThemeContext = createContext();
// Hook pour l'utiliser facilement
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
};
// Provider
export const ThemeProvider = ({ children }) => {
const [isDark, setIsDark] = useState(() => {
const saved = localStorage.getItem('theme');
if (saved) return saved === 'dark';
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
});
useEffect(() => {
localStorage.setItem('theme', isDark ? 'dark' : 'light');
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [isDark]);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e) => setIsDark(e.matches);
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
const toggleTheme = () => setIsDark(!isDark);
return (
<ThemeContext.Provider value={{ isDark, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};

231
src/data/ItemsFilter.js Normal file
View file

@ -0,0 +1,231 @@
// components/filter/ItemsFilter.js
/**
* Filtre les items par période
* @param {Array} items - Liste des items à filtrer
* @param {string|object} period - Période de filtre ('today', 'week', 'month', etc. ou { id: 'custom', start, end })
* @param {string} dateField - Nom du champ date (default: 'date')
* @returns {Array} Items filtrés
*/
export const filterItemByPeriod = (items, period, dateField = 'date') => {
if (!items || items.length === 0) return [];
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
// Période personnalisée
if (typeof period === 'object' && period.id === 'custom') {
const startDate = new Date(period.start);
startDate.setHours(0, 0, 0, 0);
const endDate = new Date(period.end);
endDate.setHours(23, 59, 59, 999);
return items.filter(item => {
const itemDate = new Date(item[dateField]);
return itemDate >= startDate && itemDate <= endDate;
});
}
// Périodes prédéfinies
let startDate, endDate;
switch (period) {
case 'today':
startDate = today;
endDate = new Date(today);
endDate.setHours(23, 59, 59, 999);
break;
case 'week':
// Début de la semaine (lundi)
const dayOfWeek = today.getDay();
const diffToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
startDate = new Date(today);
startDate.setDate(today.getDate() - diffToMonday);
endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6);
endDate.setHours(23, 59, 59, 999);
break;
case 'month':
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
break;
case 'last_month':
startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
endDate = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
break;
case 'quarter':
const currentQuarter = Math.floor(now.getMonth() / 3);
startDate = new Date(now.getFullYear(), currentQuarter * 3, 1);
endDate = new Date(now.getFullYear(), currentQuarter * 3 + 3, 0, 23, 59, 59, 999);
break;
case 'year':
startDate = new Date(now.getFullYear(), 0, 1);
endDate = new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999);
break;
case '30days':
startDate = new Date(today);
startDate.setDate(today.getDate() - 30);
endDate = new Date(today);
endDate.setHours(23, 59, 59, 999);
break;
case '90days':
startDate = new Date(today);
startDate.setDate(today.getDate() - 90);
endDate = new Date(today);
endDate.setHours(23, 59, 59, 999);
break;
case 'all':
default:
return items;
}
return items.filter(item => {
const itemDate = new Date(item[dateField]);
return itemDate >= startDate && itemDate <= endDate;
});
};
/**
* Récupère les items de la période précédente (pour comparaison)
* @param {Array} items - Liste des items
* @param {string|object} period - Période actuelle
* @param {string} dateField - Nom du champ date
* @returns {Array} Items de la période précédente
*/
export const getPreviousPeriodItems = (items, period, dateField = 'date') => {
if (!items || items.length === 0) return [];
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
// Période personnalisée - calculer la période précédente de même durée
if (typeof period === 'object' && period.id === 'custom') {
const startDate = new Date(period.start);
const endDate = new Date(period.end);
const duration = endDate.getTime() - startDate.getTime();
const previousEnd = new Date(startDate.getTime() - 1); // Jour avant le début
previousEnd.setHours(23, 59, 59, 999);
const previousStart = new Date(previousEnd.getTime() - duration);
previousStart.setHours(0, 0, 0, 0);
return items.filter(item => {
const itemDate = new Date(item[dateField]);
return itemDate >= previousStart && itemDate <= previousEnd;
});
}
let startDate, endDate;
switch (period) {
case 'today':
// Hier
startDate = new Date(today);
startDate.setDate(today.getDate() - 1);
endDate = new Date(startDate);
endDate.setHours(23, 59, 59, 999);
break;
case 'week':
// Semaine précédente
const dayOfWeek = today.getDay();
const diffToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
startDate = new Date(today);
startDate.setDate(today.getDate() - diffToMonday - 7);
endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6);
endDate.setHours(23, 59, 59, 999);
break;
case 'month':
// Mois précédent
startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
endDate = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
break;
case 'last_month':
// Mois d'avant le mois dernier
startDate = new Date(now.getFullYear(), now.getMonth() - 2, 1);
endDate = new Date(now.getFullYear(), now.getMonth() - 1, 0, 23, 59, 59, 999);
break;
case 'quarter':
// Trimestre précédent
const currentQuarter = Math.floor(now.getMonth() / 3);
startDate = new Date(now.getFullYear(), (currentQuarter - 1) * 3, 1);
endDate = new Date(now.getFullYear(), currentQuarter * 3, 0, 23, 59, 59, 999);
break;
case 'year':
// Année précédente
startDate = new Date(now.getFullYear() - 1, 0, 1);
endDate = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 999);
break;
case '30days':
// 30 jours avant les 30 derniers jours
startDate = new Date(today);
startDate.setDate(today.getDate() - 60);
endDate = new Date(today);
endDate.setDate(today.getDate() - 31);
endDate.setHours(23, 59, 59, 999);
break;
case '90days':
// 90 jours avant les 90 derniers jours
startDate = new Date(today);
startDate.setDate(today.getDate() - 180);
endDate = new Date(today);
endDate.setDate(today.getDate() - 91);
endDate.setHours(23, 59, 59, 999);
break;
case 'all':
default:
return [];
}
return items.filter(item => {
const itemDate = new Date(item[dateField]);
return itemDate >= startDate && itemDate <= endDate;
});
};
/**
* Obtient le label de la période
* @param {string|object} period - Période
* @returns {string} Label formaté
*/
export const getPeriodLabel = (period) => {
if (typeof period === 'object' && period.id === 'custom') {
const formatDate = (dateStr) => {
const d = new Date(dateStr);
return d.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
};
return `${formatDate(period.start)} - ${formatDate(period.end)}`;
}
const labels = {
today: "Aujourd'hui",
week: 'Cette semaine',
month: 'Ce mois-ci',
last_month: 'Mois dernier',
quarter: 'Ce trimestre',
year: 'Cette année',
'30days': '30 derniers jours',
'90days': '90 derniers jours',
all: 'Tout',
};
return labels[period] || 'Période';
};

373
src/data/mockData.js Normal file
View file

@ -0,0 +1,373 @@
// Comprehensive Mock Data Expansion
export const CompanyInfo = {
name: "Sage",
email: "sage@gmail.com",
adresse: "12 Avenue de l'Innovation",
adresse2: "75011 Paris, France",
code_postal: "75008",
ville: "Paris",
siret: "123 456 789 00012",
tva_intra: "FR12345678901",
telephone: "01 23 45 67 89",
}
export const currentUser = {
id: 1,
name: 'Jean Dupont',
email: 'jean.dupont@bijou.com',
role: 'Administrateur',
department: 'Direction',
phone: '+33 6 12 34 56 78',
avatar: 'JD',
preferences: {
language: 'fr',
currency: 'EUR',
theme: 'light',
notifications: { email: true, sms: false, inApp: true }
}
};
export const mockUsers = [
{ id: 1, name: 'Jean Dupont', email: 'jean.dupont@bijou.com', role: 'Administrateur', department: 'Direction', avatar: 'JD', status: 'active' },
{ id: 2, name: 'Marie Dubois', email: 'marie.dubois@bijou.com', role: 'Commercial', department: 'Ventes', avatar: 'MD', status: 'active' },
{ id: 3, name: 'Thomas Robert', email: 'thomas.robert@bijou.com', role: 'Commercial', department: 'Ventes', avatar: 'TR', status: 'active' },
{ id: 4, name: 'Sophie Martin', email: 'sophie.martin@bijou.com', role: 'Support', department: 'SAV', avatar: 'SM', status: 'active' },
{ id: 5, name: 'Pierre Durand', email: 'pierre.durand@bijou.com', role: 'Comptabilité', department: 'Finance', avatar: 'PD', status: 'active' },
{ id: 6, name: 'Julie Moreau', email: 'julie.moreau@bijou.com', role: 'Logistique', department: 'Opérations', avatar: 'JM', status: 'active' },
];
export const mockProductFamilies = [
{ id: 1, code: 'FAM-001', name: 'Ordinateurs Portables', color: 'bg-blue-100 text-blue-800', articleCount: 12, category: 'Matériel', status: 'active' },
{ id: 2, code: 'FAM-002', name: 'Écrans & Moniteurs', color: 'bg-purple-100 text-purple-800', articleCount: 8, category: 'Matériel', status: 'active' },
{ id: 3, code: 'FAM-003', name: 'Logiciels & Licences', color: 'bg-green-100 text-green-800', articleCount: 25, category: 'Software', status: 'active' },
{ id: 4, code: 'FAM-004', name: 'Services & Maintenance', color: 'bg-orange-100 text-orange-800', articleCount: 5, category: 'Service', status: 'active' },
{ id: 5, code: 'FAM-005', name: 'Accessoires', color: 'bg-gray-100 text-gray-800', articleCount: 45, category: 'Accessoire', status: 'inactive' },
];
export const mockArticles = Array.from({ length: 50 }, (_, i) => {
const family = mockProductFamilies[i % mockProductFamilies.length];
const costPrice = Math.floor(Math.random() * 1000) + 50;
const margin = 1.3 + (Math.random() * 0.4); // 30% to 70% margin
return {
id: i + 1,
reference: `ART-${(i + 1000).toString()}`,
name: `${family.name.split(' ')[0]} Pro ${2025 + (i % 3)} - Modèle ${String.fromCharCode(65 + (i % 26))}`,
familyId: family.id,
familyName: family.name,
familyColor: family.color,
costPrice: costPrice,
sellingPrice: Math.floor(costPrice * margin),
vatRate: 20,
unit: 'Pcs',
stock: Math.floor(Math.random() * 100),
minStock: 10,
status: i % 10 === 0 ? 'inactive' : 'active',
description: "Description détaillée de l'article pour les devis et factures.",
createdAt: new Date(Date.now() - Math.floor(Math.random() * 10000000000)).toISOString().split('T')[0],
};
});
export const mockClients = Array.from({ length: 25 }, (_, i) => ({
id: i + 1,
name: [`Sophie Martin`, `Pierre Durand`, `Marie Lambert`, `Lucas Bernard`, `Emma Petit`][i % 5],
company: [`ACME Corp`, `TechStart SAS`, `Digital Plus`, `InnovCo`, `WebAgency`, `LogiTrans`, `BuildIt`, `GreenEnergy`, `FoodProc`, `HealthCare`][i % 10],
email: `contact${i}@company.com`,
phone: `+33 1 ${Math.floor(Math.random() * 90 + 10)} ${Math.floor(Math.random() * 90 + 10)} ${Math.floor(Math.random() * 90 + 10)} ${Math.floor(Math.random() * 90 + 10)}`,
status: ['active', 'inactive', 'prospect', 'vip'][Math.floor(Math.random() * 4)],
owner: mockUsers[Math.floor(Math.random() * 3)].name,
industry: ['Technology', 'Finance', 'Retail', 'Manufacturing'][Math.floor(Math.random() * 4)],
city: ['Paris', 'Lyon', 'Marseille', 'Bordeaux', 'Lille'][Math.floor(Math.random() * 5)],
address: `${Math.floor(Math.random() * 100) + 1} Rue de l'Innovation, 75001 Paris`,
createdAt: new Date(Date.now() - Math.floor(Math.random() * 10000000000)).toISOString().split('T')[0],
lastActivity: new Date(Date.now() - Math.floor(Math.random() * 1000000000)).toISOString().split('T')[0]
}));
export const mockContacts = Array.from({ length: 30 }, (_, i) => ({
id: i + 1,
firstName: ['Jean', 'Marie', 'Paul', 'Sophie', 'Luc'][i % 5],
lastName: ['Dupont', 'Durand', 'Martin', 'Bernard', 'Petit'][i % 5],
email: `contact${i}@example.com`,
phone: `+33 6 ${Math.floor(Math.random() * 90 + 10)} ${Math.floor(Math.random() * 90 + 10)} ${Math.floor(Math.random() * 90 + 10)} ${Math.floor(Math.random() * 90 + 10)}`,
role: ['CEO', 'CTO', 'CFO', 'Manager', 'Buyer'][i % 5],
clientId: (i % mockClients.length) + 1
}));
export const mockSuppliers = Array.from({ length: 15 }, (_, i) => ({
id: i + 1,
name: [`Office Depot`, `Dell`, `Microsoft`, `Amazon AWS`, `Google`, `Lyreco`, `Metro`, `Rexel`, `Manutan`, `Orange`][i % 10],
contact: [`Jean Supply`, `Marie Vente`, `Paul Achat`][i % 3],
email: `supplier${i}@partner.com`,
phone: `+33 1 99 88 77 ${i.toString().padStart(2, '0')}`,
status: ['active', 'inactive', 'pending'][Math.floor(Math.random() * 3)],
category: ['Hardware', 'Software', 'Office', 'Services'][Math.floor(Math.random() * 4)],
paymentTerms: '30 jours fin de mois'
}));
const stages = ['new', 'qualification', 'discovery', 'proposal', 'negotiation', 'won', 'lost'];
export const mockOpportunities = Array.from({ length: 25 }, (_, i) => ({
id: i + 1,
name: `Projet ${['Digital', 'Cloud', 'Migration', 'Audit', 'Formation', 'Maintenance'][i % 6]} ${2025 + i}`,
client: mockClients[i % mockClients.length].company,
clientId: (i % mockClients.length) + 1,
contactId: (i % mockContacts.length) + 1,
amount: Math.floor(Math.random() * 50000) + 5000,
probability: Math.floor(Math.random() * 10) * 10,
stage: stages[Math.floor(Math.random() * 7)],
priority: ['low', 'medium', 'high'][Math.floor(Math.random() * 3)],
owner: mockUsers[Math.floor(Math.random() * 3)].name,
createdAt: new Date(Date.now() - Math.floor(Math.random() * 8000000000)).toISOString().split('T')[0],
closeDate: new Date(Date.now() + Math.floor(Math.random() * 8000000000)).toISOString().split('T')[0],
nextAction: 'Relance client',
description: 'Projet stratégique pour le développement de la nouvelle infrastructure cloud du client.',
quotes: []
}));
export const mockQuotes = Array.from({ length: 15 }, (_, i) => ({
id: i + 1,
number: `DEV-2025-${(i + 1).toString().padStart(3, '0')}`,
client: mockClients[i % mockClients.length].company,
clientId: (i % mockClients.length) + 1,
opportunityId: (i % mockOpportunities.length) + 1,
date: new Date(Date.now() - Math.floor(Math.random() * 5000000000)).toISOString().split('T')[0],
amountHT: Math.floor(Math.random() * 20000) + 1000,
status: ['draft', 'sent', 'accepted', 'refused', 'expired'][Math.floor(Math.random() * 5)],
owner: mockUsers[Math.floor(Math.random() * 3)].name,
items: [
{ description: "Consulting Senior", quantity: 2, unitPrice: 1200, vat: 20 },
{ description: "Licence Logiciel", quantity: 1, unitPrice: 5000, vat: 20 }
]
})).map(q => ({ ...q, vat: q.amountHT * 0.2, amountTTC: q.amountHT * 1.2 }));
export const mockOrders = mockQuotes.filter(q => q.status === 'accepted').map((q, i) => ({
id: i + 1,
number: `CMD-2025-${(i + 1).toString().padStart(3, '0')}`,
sourceQuote: q.number,
client: q.client,
clientId: q.clientId,
date: new Date(new Date(q.date).getTime() + 86400000 * 2).toISOString().split('T')[0],
deliveryDate: new Date(new Date(q.date).getTime() + 86400000 * 15).toISOString().split('T')[0],
amountHT: q.amountHT,
amountTTC: q.amountTTC,
status: ['pending', 'validated', 'shipped', 'delivered'][Math.floor(Math.random() * 4)],
items: q.items
}));
// ENRICHED INVOICES MOCK DATA
const invoiceStatuses = ['draft', 'sent', 'paid', 'partial', 'overdue', 'cancelled'];
export const mockInvoices = Array.from({ length: 25 }, (_, i) => {
const client = mockClients[i % mockClients.length];
const status = invoiceStatuses[Math.floor(Math.random() * invoiceStatuses.length)];
const amountHT = Math.floor(Math.random() * 15000) + 500;
const vat = amountHT * 0.2;
const amountTTC = amountHT + vat;
const date = new Date(Date.now() - Math.floor(Math.random() * 4000000000)).toISOString().split('T')[0];
const dueDate = new Date(new Date(date).getTime() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
let balanceDue = amountTTC;
let payments = [];
if (status === 'paid') {
balanceDue = 0;
payments.push({ id: 1, date: dueDate, amount: amountTTC, method: 'Virement', reference: 'VIR-12345' });
} else if (status === 'partial') {
const partialAmount = Math.floor(amountTTC * 0.4);
balanceDue = amountTTC - partialAmount;
payments.push({ id: 1, date: new Date(date).toISOString().split('T')[0], amount: partialAmount, method: 'Virement', reference: 'ACCOMPTE' });
}
return {
id: i + 1,
number: `FAC-2025-${(i + 100).toString().padStart(3, '0')}`,
client: client.company,
clientId: client.id,
clientAvatar: client.name.charAt(0),
sourceDocument: i % 3 === 0 ? `CMD-2025-00${(i % 5) + 1}` : null,
date: date,
dueDate: dueDate,
amountHT: amountHT,
vat: vat,
amountTTC: amountTTC,
balanceDue: balanceDue,
status: status,
items: [
{ id: 1, description: "Prestation de service informatique", quantity: 1, unitPrice: amountHT * 0.6, vat: 20, total: amountHT * 0.6 },
{ id: 2, description: "Matériel divers", quantity: 2, unitPrice: amountHT * 0.2, vat: 20, total: amountHT * 0.4 }
],
payments: payments,
timeline: [
{ date: date, title: 'Facture créée', type: 'creation', user: 'Jean Dupont' },
...(status !== 'draft' ? [{ date: date, title: 'Facture envoyée', type: 'email', user: 'Jean Dupont' }] : []),
...(status === 'paid' ? [{ date: dueDate, title: 'Paiement reçu', type: 'payment', user: 'Système' }] : [])
]
};
});
export const mockPurchaseOrders = Array.from({ length: 12 }, (_, i) => ({
id: i + 1,
number: `BC-2025-${(i + 100).toString()}`,
supplier: mockSuppliers[i % mockSuppliers.length].name,
supplierId: (i % mockSuppliers.length) + 1,
date: new Date(Date.now() - Math.floor(Math.random() * 5000000000)).toISOString().split('T')[0],
deliveryDate: new Date(Date.now() + Math.floor(Math.random() * 2000000000)).toISOString().split('T')[0],
amountHT: Math.floor(Math.random() * 10000) + 500,
status: ['draft', 'sent', 'received', 'invoiced'][Math.floor(Math.random() * 4)],
items: [
{ description: 'MacBook Pro 16"', quantity: 2, unitPrice: 2400 },
{ description: 'Dell Monitor 27"', quantity: 5, unitPrice: 350 }
]
})).map(o => ({ ...o, amountTTC: o.amountHT * 1.2 }));
export const mockReceptionNotes = mockPurchaseOrders.filter(p => p.status === 'received' || p.status === 'invoiced').map((p, i) => ({
id: i + 1,
number: `BR-2025-${(i + 100).toString()}`,
purchaseOrder: p.number,
supplier: p.supplier,
date: new Date(new Date(p.date).getTime() + 86400000 * 5).toISOString().split('T')[0],
status: 'validated',
items: p.items
}));
export const mockPurchaseInvoices = mockReceptionNotes.slice(0, 8).map((br, i) => ({
id: i + 1,
number: `FA-2025-${(i + 500).toString()}`,
supplier: br.supplier,
receptionNote: br.number,
date: new Date(new Date(br.date).getTime() + 86400000 * 2).toISOString().split('T')[0],
dueDate: new Date(new Date(br.date).getTime() + 86400000 * 32).toISOString().split('T')[0],
amountTTC: Math.floor(Math.random() * 10000) + 500,
status: ['pending', 'paid', 'overdue'][Math.floor(Math.random() * 3)]
}));
export const mockTickets = Array.from({ length: 15 }, (_, i) => ({
id: i + 1,
number: `TKT-${(1000 + i)}`,
client: mockClients[i % mockClients.length].company,
clientId: (i % mockClients.length) + 1,
subject: ['Erreur connexion', 'Demande évolution', 'Question facture', 'Bug critique', 'Formation', 'Accès perdu'][i % 6],
priority: ['low', 'normal', 'high', 'critical'][Math.floor(Math.random() * 4)],
status: ['open', 'in-progress', 'pending', 'resolved', 'closed'][Math.floor(Math.random() * 5)],
assignedTo: mockUsers[3 + Math.floor(Math.random() * 3)].name,
openDate: new Date(Date.now() - Math.floor(Math.random() * 1000000000)).toISOString().split('T')[0],
category: ['Technique', 'Commercial', 'Administratif'][Math.floor(Math.random() * 3)],
description: "Le client rencontre des difficultés pour accéder à la plateforme depuis la mise à jour de lundi.",
comments: [
{ user: 'Jean Dupont', date: '2025-12-01', text: 'J\'ai pris en compte la demande.' },
{ user: 'Sophie Martin', date: '2025-12-02', text: 'Le problème semble venir du cache navigateur.' }
]
}));
export const mockTimeline = Array.from({ length: 20 }, (_, i) => ({
id: i,
type: ['email', 'call', 'meeting', 'quote', 'task'][Math.floor(Math.random() * 5)],
title: ['Email envoyé', 'Appel client', 'Réunion projet', 'Devis créé', 'Tâche terminée'][Math.floor(Math.random() * 5)],
description: 'Détails de l\'activité simulée pour la démonstration.',
date: new Date(Date.now() - Math.floor(Math.random() * 1000000000)).toISOString().split('T')[0],
user: mockUsers[Math.floor(Math.random() * mockUsers.length)].name
}));
export const mockStats = {
revenue: [
{ name: 'Jan', value: 145000 }, { name: 'Fév', value: 168000 }, { name: 'Mar', value: 156000 },
{ name: 'Avr', value: 178000 }, { name: 'Mai', value: 192000 }, { name: 'Juin', value: 215000 },
{ name: 'Juil', value: 198000 }, { name: 'Août', value: 185000 }, { name: 'Sep', value: 225000 },
{ name: 'Oct', value: 238000 }, { name: 'Nov', value: 245000 }, { name: 'Déc', value: 260000 }
],
expenses: [
{ name: 'Jan', value: 110000 }, { name: 'Fév', value: 115000 }, { name: 'Mar', value: 112000 },
{ name: 'Avr', value: 125000 }, { name: 'Mai', value: 128000 }, { name: 'Juin', value: 135000 },
{ name: 'Juil', value: 130000 }, { name: 'Août', value: 128000 }, { name: 'Sep', value: 140000 },
{ name: 'Oct', value: 145000 }, { name: 'Nov', value: 150000 }, { name: 'Déc', value: 155000 }
],
ticketPriority: [
{ name: 'Basse', value: 25, fill: '#3b82f6' },
{ name: 'Normale', value: 45, fill: '#9ca3af' },
{ name: 'Haute', value: 20, fill: '#f97316' },
{ name: 'Critique', value: 10, fill: '#ef4444' }
],
ticketStatus: [
{ name: 'Ouvert', value: 15, fill: '#ef4444' },
{ name: 'En cours', value: 25, fill: '#f97316' },
{ name: 'Attente', value: 10, fill: '#eab308' },
{ name: 'Résolu', value: 40, fill: '#22c55e' },
{ name: 'Fermé', value: 10, fill: '#9ca3af' }
],
quoteStatus: [
{ name: 'Brouillon', value: 15, fill: '#9ca3af' },
{ name: 'Envoyé', value: 30, fill: '#3b82f6' },
{ name: 'Accepté', value: 45, fill: '#22c55e' },
{ name: 'Refusé', value: 10, fill: '#ef4444' }
],
salesByRep: [
{ name: 'Jean', value: 450000 },
{ name: 'Marie', value: 380000 },
{ name: 'Thomas', value: 320000 }
]
};
// --- KPI HELPERS ---
export const filterDataByPeriod = (data, dateField, period) => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
// Handle Custom Range Object
if (typeof period === 'object' && period?.id === 'custom') {
if (!period.start || !period.end) return data;
const startDate = new Date(period.start);
const endDate = new Date(period.end);
// Set end date to end of day for inclusive comparison
endDate.setHours(23, 59, 59, 999);
return data.filter(item => {
const itemDate = new Date(item[dateField]);
if (isNaN(itemDate)) return false;
return itemDate >= startDate && itemDate <= endDate;
});
}
// Handle string identifiers
return data.filter(item => {
const itemDate = new Date(item[dateField]);
if (isNaN(itemDate)) return false;
switch (period) {
case 'today':
return itemDate >= today;
case 'week':
const startOfWeek = new Date(today);
startOfWeek.setDate(today.getDate() - today.getDay()); // Sunday
return itemDate >= startOfWeek;
case 'month':
return itemDate.getMonth() === today.getMonth() && itemDate.getFullYear() === today.getFullYear();
case 'quarter':
const currentQuarter = Math.floor(today.getMonth() / 3);
const itemQuarter = Math.floor(itemDate.getMonth() / 3);
return itemQuarter === currentQuarter && itemDate.getFullYear() === today.getFullYear();
case 'year':
return itemDate.getFullYear() === today.getFullYear();
case '30days':
const thirtyDaysAgo = new Date(today);
thirtyDaysAgo.setDate(today.getDate() - 30);
return itemDate >= thirtyDaysAgo;
default: // 'all'
return true;
}
});
};
export const calculateKPIs = (data, period, config) => {
const filtered = filterDataByPeriod(data, config.dateField || 'date', period);
// Determine trend seed
const seed = typeof period === 'object' ? (period.start?.length || 0) : period.length;
return {
filteredCount: filtered.length,
totalAmount: filtered.reduce((acc, item) => acc + (item[config.amountField] || 0), 0),
trend: seed % 2 === 0 ? 'up' : 'down',
items: filtered
};
};

110
src/index.css Normal file
View file

@ -0,0 +1,110 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 346 87% 30%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 346 87% 30%;
--radius: 0.5rem;
--primary-color: #000000;
--primary-color-dark: #007E45;
/* --primary-color: #C94635;
--primary-color-dark: #941403; */
--error-color: #C94635;
--background-color: #ffffff;
--text-color: #6f6f6f;
}
.dark {
--primary-color: #941403;
--background-color: #1a1a1a;
--text-color: #ffffff;
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 346 87% 30%;
--primary-foreground: 0 0% 98%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 346 87% 30%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
/* Unified Color Utilities - All mapped to #338660 */
.text-\[\#941403\], .text-red-700, .text-\[\#A71810\], .text-\[\#00DB00\] {
@apply text-[#338660];
}
.bg-\[\#941403\], .bg-red-700, .bg-\[\#A71810\], .bg-\[\#00DB00\] {
@apply bg-[#338660];
}
.border-\[\#941403\], .border-red-700, .border-\[\#A71810\], .border-\[\#00DB00\] {
@apply border-[#338660];
}
.ring-\[\#941403\], .ring-\[\#A71810\], .ring-\[\#00DB00\] {
@apply ring-[#338660];
}
}
body {
font-family: 'Sage Text', sans-serif;
}
h1, h2, h3 {
font-family: 'Sage Headline', sans-serif;
}
/* Custom Scrollbar */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 20px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: rgba(107, 114, 128, 0.8);
}

2
src/lib/data.ts Normal file
View file

@ -0,0 +1,2 @@
export const ACCESS_TOKEN = "access_token";
export const REFRESH_TOKEN = "refresh_token";

47
src/lib/utils.js Normal file
View file

@ -0,0 +1,47 @@
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
export const formatDateFR = (dateString) => {
if (!dateString) return "";
const date = new Date(dateString);
return date.toLocaleDateString("fr-FR", {
day: "numeric",
month: "long",
year: "numeric",
});
};
export const formatCurrency = (amount) => {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 2
}).format(amount);
};
export const formatDateFRCourt = (dateString) => {
if (!dateString) return "";
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${day}/${month}/${year}`;
};
export const formatForDateInput = (dateString) => {
if (!dateString) return ''
return dateString.split(' ')[0]
}

13
src/main.jsx Normal file
View file

@ -0,0 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from '@/App';
import '@/index.css';
import StoreProvider from './providers/StoreProvider';
import './assets/fonts/fonts.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<StoreProvider>
<App />
</StoreProvider>
);

View file

@ -0,0 +1,19 @@
/* eslint-disable @typescript-eslint/consistent-type-imports */
/* eslint-disable import/no-unresolved */
import { store } from '@/store/store';
import { useRef } from 'react';
import { Provider } from 'react-redux';
export default function StoreProvider({
children,
}: {
children: React.ReactNode;
}) {
return <Provider store={store}>{children}</Provider>;
}

179
src/routes/DatavenRoute.jsx Normal file
View file

@ -0,0 +1,179 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { Toaster } from '@/components/ui/toaster';
import { ThemeProvider } from '@/contexts/ThemeContext';
import AppLayout from '@/components/layout/AppLayout';
// Dashboard
import DashboardPage from '@/pages/DashboardPage';
// CRM
import ProspectsPage from '@/pages/crm/ProspectsPage';
import CreateProspectPage from '@/pages/crm/CreateProspectPage';
import ProspectDetailPage from '@/pages/crm/ProspectDetailPage';
import ClientsPage from '@/pages/crm/ClientsPage';
import ClientDetailPage from '@/pages/crm/ClientDetailPage';
import SuppliersPage from '@/pages/crm/SuppliersPage';
import OpportunitiesPipelinePage from '@/pages/crm/OpportunitiesPipelinePage';
import OpportunityDetailPage from '@/pages/crm/OpportunityDetailPage';
import ActivitiesPage from '@/pages/crm/ActivitiesPage';
import TasksPage from '@/pages/crm/TasksPage';
// Tiers (Articles & Families)
import ArticlesPage from '@/pages/tiers/ArticlesPage';
import ArticleDetailPage from '@/pages/tiers/ArticleDetailPage';
import ProductFamiliesPage from '@/pages/tiers/ProductFamiliesPage';
import ProductFamilyDetailPage from '@/pages/tiers/ProductFamilyDetailPage';
// Sales
import QuotesPage from '@/pages/sales/QuotesPage';
import QuoteDetailPage from '@/pages/sales/QuoteDetailPage';
import OrdersPage from '@/pages/sales/OrdersPage';
import OrderDetailPage from '@/pages/sales/OrderDetailPage';
import DeliveryNotesPage from '@/pages/sales/DeliveryNotesPage';
import InvoicesPage from '@/pages/sales/InvoicesPage';
import InvoiceDetailPage from '@/pages/sales/InvoiceDetailPage';
import CreditNotesPage from '@/pages/sales/CreditNotesPage';
// Purchases
import PurchaseOrdersPage from '@/pages/purchases/PurchaseOrdersPage';
import PurchaseOrderDetailPage from '@/pages/purchases/PurchaseOrderDetailPage';
import ReceptionNotesPage from '@/pages/purchases/ReceptionNotesPage';
import ReceptionNoteDetailPage from '@/pages/purchases/ReceptionNoteDetailPage';
import PurchaseInvoicesPage from '@/pages/purchases/PurchaseInvoicesPage';
import PurchaseInvoiceDetailPage from '@/pages/purchases/PurchaseInvoiceDetailPage';
// Support
import SupportDashboardPage from '@/pages/support/SupportDashboardPage';
import TicketsPage from '@/pages/support/TicketsPage';
import TicketDetailPage from '@/pages/support/TicketDetailPage';
// Documents
import DocumentsPage from '@/pages/DocumentsPage';
// Admin & User
import SettingsPage from '@/pages/admin/SettingsPage';
import UsersPage from '@/pages/admin/UsersPage';
import RolesPage from '@/pages/admin/RolesPage';
import ActivityLogPage from '@/pages/admin/ActivityLogPage';
import UserProfilePage from '@/pages/UserProfilePage';
// UI Kit
import UIKitPage from '@/pages/UIKitPage';
import SuppliersDetailPage from '@/pages/crm/SuppliersDetailPage';
import CreditNotesDetailPage from '@/pages/sales/CreditNotesDetailPage';
import DeliveryNotesDetailPage from '@/pages/sales/DeliveryNotesDetailPage';
import CreateClientPage from '@/pages/crm/CreateClientPage';
import SignatureDashboard from '@/pages/signature/SignatureDashboard';
import SignatureTracking from '@/pages/signature/SignatureTracking';
import SignatureCreditPurchase from '@/pages/signature/SignatureCreditPurchase';
import CommercialPage from '@/pages/tiers/CommercialPage';
import QuoteCreatePage from '@/pages/sales/QuoteCreate';
import InvoiceCreatePage from '@/pages/sales/InvoiceCreatePage';
import SageBuilderPage from '@/pages/SageBuilderPage';
import PaymentsPage from '@/pages/sales/PaymentsPage';
const DatavenRoute = () => {
return (
<AppLayout>
<Routes>
{/* Dashboard */}
<Route path="/" element={<DashboardPage />} />
{/* CRM */}
<Route path="/prospects" element={<ProspectsPage />} />
<Route path="/prospects/create" element={<CreateProspectPage />} />
<Route path="/prospects/:id" element={<ProspectDetailPage />} />
<Route path="/clients" element={<ClientsPage />} />
<Route path="/clients/create" element={<CreateClientPage />} />
<Route path="/clients/:id/edit" element={<CreateClientPage />} />
<Route path="/clients/:id" element={<ClientDetailPage />} />
<Route path="/fournisseurs" element={<SuppliersPage />} />
<Route path="/fournisseurs/:id" element={<SuppliersDetailPage />} />
<Route path="/commercial" element={<CommercialPage />} />
<Route path="/reglements" element={<PaymentsPage />} />
{/* <Route path="/reglements/:id" element={<PaymentDetailPage />} /> */}
{/* Tiers - Articles & Familles */}
<Route path="/articles" element={<ArticlesPage />} />
<Route path="/articles/:id" element={<ArticleDetailPage />} />
<Route path="/familles-articles" element={<ProductFamiliesPage />} />
<Route path="/familles-articles/:id" element={<ProductFamilyDetailPage />} />
{/* iframe */}
<Route path="/sage-builder" element={<SageBuilderPage />} />
{/* Sales */}
<Route path="/opportunites" element={<OpportunitiesPipelinePage />} />
<Route path="/opportunites/:id" element={<OpportunityDetailPage />} />
<Route path="/pipeline" element={<OpportunitiesPipelinePage />} />
<Route path="/activites" element={<ActivitiesPage />} />
<Route path="/taches" element={<TasksPage />} />
<Route path="/devis" element={<QuotesPage />} />
<Route path="/devis/:id" element={<QuoteDetailPage />} />
<Route path="/devis/nouveau" element={<QuoteCreatePage />} />
<Route path="/commandes" element={<OrdersPage />} />
<Route path="/commandes/:id" element={<OrderDetailPage />} />
<Route path="/bons-livraison" element={<DeliveryNotesPage />} />
<Route path="/bons-livraison/:id" element={<DeliveryNotesDetailPage />} />
<Route path="/factures" element={<InvoicesPage />} />
<Route path="/factures/:id" element={<InvoiceDetailPage />} />
<Route path="/factures/nouveau" element={<InvoiceCreatePage />} />
<Route path="/avoirs" element={<CreditNotesPage />} />
<Route path="/avoirs/:id" element={<CreditNotesDetailPage />} />
{/* Signature Électronique */}
<Route path="/signature/dashboard" element={<SignatureDashboard />} />
<Route path="/signature/tracking" element={<SignatureTracking />} />
<Route path="/signature/purchase" element={<SignatureCreditPurchase />} />
{/* Purchases */}
<Route path="/bons-commande" element={<PurchaseOrdersPage />} />
<Route path="/bons-commande/:id" element={<PurchaseOrderDetailPage />} />
<Route path="/bons-reception" element={<ReceptionNotesPage />} />
<Route path="/bons-reception/:id" element={<ReceptionNoteDetailPage />} />
<Route path="/factures-achat" element={<PurchaseInvoicesPage />} />
<Route path="/factures-achat/:id" element={<PurchaseInvoiceDetailPage />} />
{/* Support */}
<Route path="/support-dashboard" element={<SupportDashboardPage />} />
<Route path="/tickets" element={<TicketsPage />} />
<Route path="/tickets/:id" element={<TicketDetailPage />} />
{/* Documents */}
<Route path="/documents" element={<DocumentsPage />} />
{/* Admin */}
<Route path="/parametres" element={<SettingsPage />} />
<Route path="/utilisateurs" element={<UsersPage />} />
<Route path="/roles" element={<RolesPage />} />
<Route path="/journal" element={<ActivityLogPage />} />
{/* User */}
<Route path="/profile" element={<UserProfilePage />} />
{/* Dev */}
<Route path="/ui-kit" element={<UIKitPage />} />
{/* Catch all */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</AppLayout>
);
}
export default DatavenRoute;

View file

@ -0,0 +1,19 @@
import { Navigate } from 'react-router-dom'
import Cookies from "js-cookie";
import { ACCESS_TOKEN, REFRESH_TOKEN } from '@/lib/data';
type Props = {
children: React.ReactNode
}
const ProtectedRoute: React.FC<Props> = ({ children }) => {
const access_token = Cookies.get(ACCESS_TOKEN);
const refresh_token = Cookies.get(REFRESH_TOKEN)
if (!access_token || ! refresh_token) return <Navigate to="/" replace />
return <>{children}</>
}
export default ProtectedRoute

86
src/service/api.ts Normal file
View file

@ -0,0 +1,86 @@
/* eslint-disable import/no-unresolved */
import { ACCESS_TOKEN, REFRESH_TOKEN } from "@/lib/data";
import axios from 'axios'
import Cookies from 'js-cookie';
export const BackUrl = import.meta.env.VITE_API_URL
const access_token: string = Cookies.get(ACCESS_TOKEN) || "";
const apiService = axios.create({
baseURL: BackUrl,
headers: {
Authorization: access_token ? `Bearer ${access_token}` : "dsqd",
}
})
// Interceptor pour capturer les erreurs et gérer le refresh token
apiService.interceptors.response.use(
response => response,
async error => {
if (!error.response) {
throw new Error('Network Error');
}
// Si le token est expiré (401), tente de le rafraîchir
if (error.response.status === 401) {
const refreshToken = Cookies.get(REFRESH_TOKEN);
if (refreshToken) {
try {
const res = await axios.post(
`${BackUrl}/auth/refresh`,
{ refresh_token: refreshToken }
);
if (res.data && res.data.success && res.data.data.access_token) {
Cookies.set(ACCESS_TOKEN, res.data.data.access_token);
error.config.headers.Authorization = `Bearer ${res.data.data.access_token}`;
return apiService.request(error.config);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (refreshErr) {
Cookies.remove(ACCESS_TOKEN);
Cookies.remove(REFRESH_TOKEN);
}
}
}
return Promise.reject(error);
}
);
// Interceptor pour capturer les erreurs
apiService.interceptors.response.use(
response => response,
error => {
if (!error.response) {
throw new Error('Network Error');
}
return Promise.reject(error);
}
)
apiService.interceptors.request.use(
request => {
const access_token: string = Cookies.get(ACCESS_TOKEN) || "";
// Assurer que le token est présent et valide
if (access_token) {
request.headers.Authorization = `Bearer ${access_token}`;
}
return request;
},
error => {
// Gérer les erreurs d'intercepteur
return Promise.reject(error);
}
);
export default apiService

509
src/service/sageService.ts Normal file
View file

@ -0,0 +1,509 @@
/* eslint-disable import/no-unresolved */
import { loginInterface, loginResponseInterface, resetPassInterface, UserInterface } from '@/types/userInterface';
import { API_ENDPOINTS } from '@/config/apiConfig';
import { pdfInterface, responseDefault, SageDocumentType, SignatureRequest, StatusRequest, StatusResponse, TransformationResponse, TypeDocument, UniversignType } from '@/types/sageTypes';
import { Client, ClientCreateResponse, ClientRequest, ClientUpdateRequest, ContactRequest, Contacts } from '@/types/clientType';
import apiService, { BackUrl } from './api';
import { Article, ArticleRequest, ArticleUpdateRequest, StockRequest, StockResponse } from '@/types/articleType';
import { DevisDetail, DevisListItem, DevisRequest, DevisResponse, DevisUpdateRequest } from '@/types/devisType';
import { Commande, CommandeRequest, CommandeResponse, CommandeUpdateRequest } from '@/types/commandeTypes';
import { Facture, FactureRequest, FactureUpdateRequest, FactureValideResponse, RelanceRequest, RelanceResponse } from '@/types/factureType';
import { BL, BLRequest, BLUpdateRequest } from '@/types/BL_Types';
import { Fournisseur, FournisseurRequest, FournisseurUpdateRequest } from '@/types/fournisseurType';
import { Avoir, AvoirRequest, AvoirUpdateRequest } from '@/types/avoirType';
import { Famille, FamilleRequest } from '@/types/familleType';
import { Gateways, GatewaysRequest, GatewaysResponse } from '@/types/gateways';
import { Commercial, CommercialRequest } from '@/types/commercialType';
import { Societe } from '@/types/societeType';
import { EntrepriseResponse } from '@/types/entreprise';
import { ComptesResponse, DeviseResponse, EncaissementResponse, ModeResponse, ReglementPostResponse, ReglementRequest, ReglementsIdResponse, reglementsResponse, TauxResponse, TresorerieResponse } from '@/types/reglementType';
export const sageService = {
// ========== Authentification ==========
async authMe(): Promise<UserInterface> {
return (await apiService.get(`${API_ENDPOINTS.auth}`)).data
},
async login(data: loginInterface): Promise<loginResponseInterface> {
const rep = await apiService.post(`${API_ENDPOINTS.login}`, data)
return rep.data
},
async forgotPass(email: string): Promise<responseDefault> {
return (await apiService.post(`${API_ENDPOINTS.forgotPassword}`, { email: email })).data
},
async resetPass(data: resetPassInterface): Promise<responseDefault> {
return (await apiService.post(`${API_ENDPOINTS.resetPassword}`, data)).data
},
// ========== contacts ==========
async contacts(numero: string): Promise<Contacts[]> {
return (await apiService.get(`${API_ENDPOINTS.contactsClient(numero)}`)).data
},
async addContact(data: ContactRequest): Promise<Contacts>{
return (await apiService.post(API_ENDPOINTS.contactsClient(data.numero!), data)).data
},
async updateContact(contact_numero: number,data: ContactRequest): Promise<Contacts> {
return (await apiService.put(API_ENDPOINTS.contactId(data.numero!, contact_numero), data)).data;
},
// ========== contacts ==========
async searchEntreprise(query: string): Promise<EntrepriseResponse> {
return (await apiService.get(`${API_ENDPOINTS.searchEntreprise(query)}`)).data
},
// ========== contacts ==========
async commercials(): Promise<Commercial[]> {
return (await apiService.get(`${API_ENDPOINTS.commercials}`)).data
},
async commercialsById(numero: string): Promise<Commercial> {
return (await apiService.get(`${API_ENDPOINTS.commercialsById(numero)}`)).data
},
async addCommercial(data: CommercialRequest): Promise<Commercial> {
return (await apiService.post(API_ENDPOINTS.commercials, data)).data
},
// async updateCommercial(contact_numero: number,data: ContactRequest): Promise<Commercial> {
// return (await apiService.put(API_ENDPOINTS.contactId(data.numero!, contact_numero), data)).data;
// },
// ========== clients ==========
async getClients(query?: string): Promise<Client[]> {
const params = query ? `?query=${encodeURIComponent(query)}` : ''
return (await apiService.get(`${API_ENDPOINTS.clients}${params}`)).data
},
async getClient(id: string): Promise<Client> {
return (await apiService.get<Client>(API_ENDPOINTS.clientById(id))).data
},
async addClient(data: ClientRequest): Promise<ClientCreateResponse> {
return (await apiService.post(API_ENDPOINTS.clients, data)).data
},
async updateClient(numero: string, data: ClientUpdateRequest): Promise<Client> {
return (await apiService.put(API_ENDPOINTS.clientById(numero), data)).data
},
async updateStatusClient(numero: string, est_actif: boolean): Promise<Client> {
const data = {
est_actif
}
return (await apiService.put(API_ENDPOINTS.clientById(numero), data)).data
},
// ========== societe ==========
async getSociete(): Promise<Societe> {
return (await apiService.get(`${API_ENDPOINTS.societe}`)).data
},
// ========== gateways ==========
async getGateways(): Promise<GatewaysResponse> {
return (await apiService.get(`${API_ENDPOINTS.gateways}`)).data
},
async getGatewaysById(id: string): Promise<Gateways> {
return (await apiService.get<Gateways>(API_ENDPOINTS.gatewaysById(id))).data
},
async addGateways(data: GatewaysRequest): Promise<Gateways> {
return (await apiService.post(API_ENDPOINTS.gateways, data)).data
},
// ========== Fournisseurs ==========
async getFournisseurs(query?: string): Promise<Fournisseur[]> {
const params = query ? `?query=${encodeURIComponent(query)}` : ''
return (await apiService.get(`${API_ENDPOINTS.fournisseurs}${params}`)).data
},
async addFournisseur(data: FournisseurRequest): Promise<Fournisseur> {
return (await apiService.post(API_ENDPOINTS.fournisseurs, data)).data
},
async updateFournisseur(numero: string, data: FournisseurUpdateRequest): Promise<Fournisseur> {
return (await apiService.put(API_ENDPOINTS.fournisseursById(numero), data)).data
},
// ========== article ==========
async getArticles(query?: string): Promise<Article[]> {
const params = query ? `?query=${encodeURIComponent(query)}` : ''
return (await apiService.get<Article[]>(`${API_ENDPOINTS.articles}${params}`)).data
},
async getArticleId(id: string): Promise<Article> {
return (await apiService.get<Article>(API_ENDPOINTS.articleById(id))).data
},
async createArticle(data: ArticleRequest): Promise<Article> {
const rep = await apiService.post<Article>(API_ENDPOINTS.articles, data)
return rep.data
},
async updateArticle(reference: string, data: ArticleUpdateRequest) {
return (await apiService.put(API_ENDPOINTS.articleById(reference), data)).data
},
// ========== add stock ==========
async addStock(data: StockRequest): Promise<StockResponse> {
const rep = await apiService.post<StockResponse>(API_ENDPOINTS.addStock, data)
return rep.data
},
// ========== famille-article ==========
async getFamilles(query?: string): Promise<Famille[]> {
const params = query ? `?query=${encodeURIComponent(query)}` : ''
return (await apiService.get(`${API_ENDPOINTS.familles}${params}`)).data
},
async createFamille(data: FamilleRequest): Promise<Famille> {
return (await apiService.post(API_ENDPOINTS.familles, data)).data
},
// ========== devis ==========
async getDevis(id: string): Promise<DevisListItem> {
return (await apiService.get<DevisListItem>(API_ENDPOINTS.devisById(id))).data
},
async createDevis(data: DevisRequest): Promise<DevisResponse> {
const rep = await apiService.post<DevisResponse>(API_ENDPOINTS.devis, data)
return rep.data
},
async updateDevis(numero: string, data: DevisUpdateRequest) {
return (await apiService.put(API_ENDPOINTS.devisById(numero), data)).data
},
async getDevisList(statut?: number, limit?: number): Promise<DevisListItem[]> {
let params = ''
if (statut !== undefined || limit) {
const queryParams = new URLSearchParams()
if (statut !== undefined) queryParams.append('statut', statut.toString())
if (limit) queryParams.append('limit', limit.toString())
params = `?${queryParams.toString()}`
}
return (await apiService.get<DevisListItem[]>(`${API_ENDPOINTS.devis}${params}`)).data
},
async changerStatut(type_doc: SageDocumentType, data: StatusRequest): Promise<StatusResponse> {
return (await apiService.put(`/document/${type_doc}/${data.numero}/statut?nouveau_statut=${data.status}`)).data
},
async devisToPDF(id: string) {
const response = await fetch(`${BackUrl}/devis/${id}/pdf`, {
method: 'GET',
headers: {
'Accept': 'application/pdf',
},
});
if (!response.ok) {
throw new Error('Erreur lors du téléchargement');
}
// ✅ Récupérer directement en tant que Blob
const blob = await response.blob();
return blob;
},
// ========== commandes ==========
async getCommandes(statut?: number, limit?: number): Promise<Commande[]> {
let params = ''
if (statut !== undefined || limit) {
const queryParams = new URLSearchParams()
if (statut !== undefined) queryParams.append('statut', statut.toString())
if (limit) queryParams.append('limit', limit.toString())
params = `?${queryParams.toString()}`
}
return (await apiService.get<Commande[]>(`/commandes${params}`)).data
},
async getCommande(id: string): Promise<Commande> {
return (await apiService.get<Commande>(`/commandes/${id}`)).data
},
async createCommande(data: CommandeRequest): Promise<CommandeResponse> {
const rep = await apiService.post<CommandeResponse>(API_ENDPOINTS.commandes, data)
return rep.data
},
async updateCommande(numero: string, data: CommandeUpdateRequest) {
return (await apiService.put(API_ENDPOINTS.commandeById(numero), data)).data
},
// ========== bon de livraison ==========
async getAllBL(statut?: number, limit?: number): Promise<BL[]> {
let params = ''
if (statut !== undefined || limit) {
const queryParams = new URLSearchParams()
if (statut !== undefined) queryParams.append('statut', statut.toString())
if (limit) queryParams.append('limit', limit.toString())
params = `?${queryParams.toString()}`
}
const rep = await apiService.get<BL[]>(`${API_ENDPOINTS.bl}${params}`)
return rep.data
},
async getBL(id: string): Promise<BL> {
return (await apiService.get<BL>(`${API_ENDPOINTS.blById(id)}`)).data
},
async createBL(data: BLRequest) {
const rep = await apiService.post(API_ENDPOINTS.bl, data)
return rep.data
},
async updateBL(numero: string, data: BLUpdateRequest) {
return (await apiService.put(API_ENDPOINTS.blById(numero), data)).data
},
// ========== Facture==========
async getFactures(statut?: number, limit?: number): Promise<Facture[]> {
let params = ''
if (statut !== undefined || limit) {
const queryParams = new URLSearchParams()
if (statut !== undefined) queryParams.append('statut', statut.toString())
if (limit) queryParams.append('limit', limit.toString())
params = `?${queryParams.toString()}`
}
return (await apiService.get<Facture[]>(`${API_ENDPOINTS.factures}${params}`)).data
},
async getFacture(id: string): Promise<Facture> {
return (await apiService.get<Facture>(`${API_ENDPOINTS.factures}/${id}`)).data
},
async relancerFacture(data: RelanceRequest): Promise<RelanceResponse> {
return apiService.post(`${API_ENDPOINTS.factureRelancer(data.factureId)}`, {
doc_id: data.factureId,
message_personnalise: data.messagePersonnalise
})
},
async createFacture(data: FactureRequest) {
const rep = await apiService.post(API_ENDPOINTS.factures, data)
return rep.data
},
async updateFacture(numero: string, data: FactureUpdateRequest) {
return (await apiService.put(API_ENDPOINTS.factureById(numero), data)).data
},
async validerFacture(id: string): Promise<FactureValideResponse> {
return (await apiService.post<FactureValideResponse>(API_ENDPOINTS.factureValider(id))).data
},
// ========== Avoirs ==========
async getAvoirs(statut?: number, limit?: number): Promise<Avoir[]> {
let params = ''
if (statut !== undefined || limit) {
const queryParams = new URLSearchParams()
if (statut !== undefined) queryParams.append('statut', statut.toString())
if (limit) queryParams.append('limit', limit.toString())
params = `?${queryParams.toString()}`
}
return (await apiService.get<Avoir[]>(`${API_ENDPOINTS.avoirs}${params}`)).data
},
async getAvoir(id: string): Promise<Avoir> {
return (await apiService.get<Avoir>(`${API_ENDPOINTS.avoirsById(id)}`)).data
},
async createAvoir(data: AvoirRequest) {
const rep = await apiService.post(API_ENDPOINTS.avoirs, data)
return rep.data
},
async updateAvoir(numero: string, data: AvoirUpdateRequest) {
return (await apiService.put(API_ENDPOINTS.avoirsById(numero), data)).data
},
// ========== workflow ==========
async devisToCommande(id: string): Promise<TransformationResponse> {
return (await apiService.post<TransformationResponse>(API_ENDPOINTS.devisToCommande(id))).data
},
async devisToFacture(id: string): Promise<TransformationResponse> {
return (await apiService.post<TransformationResponse>(API_ENDPOINTS.devisToFacture(id))).data
},
async commandeToBL(id: string): Promise<TransformationResponse> {
return (await apiService.post<TransformationResponse>(API_ENDPOINTS.commandeToBL(id))).data
},
async commandeToFacture(id: string): Promise<TransformationResponse> {
return (await apiService.post<TransformationResponse>(API_ENDPOINTS.commandeToFacture(id))).data
},
async blToFacture(id: string): Promise<TransformationResponse> {
return (await apiService.post<TransformationResponse>(API_ENDPOINTS.blToFacture(id))).data
},
// ========== trans PDF ==========
async transPDF(data: pdfInterface) {
const response = await fetch(`${BackUrl}/documents/${data.type_doc}/${data.numero}/pdf`, {
method: 'GET',
headers: {
'Accept': 'application/pdf',
},
});
if (!response.ok) {
throw new Error('Erreur lors du téléchargement');
}
// ✅ Récupérer directement en tant que Blob
const blob = await response.blob();
return blob;
},
async envoyerDevisSignature(id: string, emailSignataire: string, nomSignataire: string): Promise<any> {
const request: SignatureRequest = {
sage_document_id: id,
sage_document_type: TypeDocument.DEVIS,
signer_email: emailSignataire,
signer_name: nomSignataire,
document_name: "test"
}
// return apiService.post(`/signature/universign/send`, request)
return (await apiService.post(`/universign/signatures/create`, request)).data
},
async universign(): Promise<UniversignType[]> {
return (await apiService.get(`/universign/transactions`)).data
},
async universignId(transaction_id: string): Promise<UniversignType> {
return (await apiService.get<UniversignType>(`/universign/transactions/${transaction_id}`)).data
},
async downloadSignedDocument(transactionId: string): Promise<Blob> {
const response = await apiService.get<Blob>(
API_ENDPOINTS.downloadSignedDocument(transactionId),
{
responseType: 'blob',
headers: {
'Accept': 'application/pdf',
},
}
);
return response.data;
},
// ========== reglement ==========
async getReglement(): Promise<reglementsResponse> {
return (await apiService.get<reglementsResponse>(API_ENDPOINTS.reglement)).data
},
async getReglementId(id: string): Promise<ReglementsIdResponse> {
return (await apiService.get<ReglementsIdResponse>(API_ENDPOINTS.reglementById(id))).data
},
async reglerFacture(data: ReglementRequest): Promise<ReglementPostResponse> {
const rep = await apiService.post<ReglementPostResponse>(API_ENDPOINTS.reglerFacture, data)
return rep.data
},
async getMode(): Promise<ModeResponse> {
return (await apiService.get<ModeResponse>(API_ENDPOINTS.mode)).data
},
async getDevises(): Promise<DeviseResponse> {
return (await apiService.get<DeviseResponse>(API_ENDPOINTS.devises)).data
},
async getTresorerie(): Promise<TresorerieResponse> {
return (await apiService.get<TresorerieResponse>(API_ENDPOINTS.tresorerie)).data
},
async getCompteGeneraux(): Promise<ComptesResponse> {
return (await apiService.get<ComptesResponse>(API_ENDPOINTS.compte_generaux)).data
},
async getTaux(): Promise<TauxResponse> {
return (await apiService.get<TauxResponse>(API_ENDPOINTS.taux)).data
},
async getEncaissement(): Promise<EncaissementResponse> {
return (await apiService.get<EncaissementResponse>(API_ENDPOINTS.encaissement)).data
},
}

View file

@ -0,0 +1,122 @@
import { mockQuotes, mockOrders, mockInvoices } from '@/data/mockData';
// Mock data store for signatures
let mockSignatures = [
...mockQuotes.filter(q => q.status !== 'Brouillon').map(q => ({
id: `SIG-Q-${q.id}`,
documentId: q.id,
documentType: 'quote',
documentNumber: q.number,
clientName: q.client,
signerEmail: q.signerEmail || 'client@email.com',
status: q.signatureStatus === 'signed' ? 'signed' : q.signatureStatus === 'viewed' ? 'viewed' : 'sent',
sentAt: q.date,
viewedAt: q.signatureStatus !== 'pending' ? new Date(new Date(q.date).getTime() + 3600000).toISOString() : null,
signedAt: q.signedDate,
creditsConsumed: q.signatureStatus === 'signed',
remindersSent: 0
})),
// Add some dummy orders/invoices for variety
{
id: 'SIG-O-1',
documentId: 1,
documentType: 'order',
documentNumber: 'CM000126',
clientName: 'TechStart SAS',
signerEmail: 'jean@techstart.com',
status: 'pending',
sentAt: new Date(Date.now() - 86400000 * 2).toISOString(),
creditsConsumed: false,
remindersSent: 1
}
];
let creditBalance = 45;
let totalPurchased = 100;
export const universignService = {
// Get dashboard statistics
getStats: async () => {
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 800));
const total = mockSignatures.length;
const signed = mockSignatures.filter(s => s.status === 'signed').length;
const pending = mockSignatures.filter(s => ['sent', 'viewed', 'pending'].includes(s.status)).length;
const refused = mockSignatures.filter(s => ['refused', 'expired'].includes(s.status)).length;
const remindable = mockSignatures.filter(s => ['sent', 'viewed'].includes(s.status)).length;
return {
credits: {
total: totalPurchased,
consumed: totalPurchased - creditBalance,
remaining: creditBalance,
projectionDays: Math.floor(creditBalance / 2) // Assuming 2 signatures per day
},
signatures: {
total,
signed,
pending,
refused,
remindable,
conversionRate: total > 0 ? Math.round((signed / total) * 100) : 0
}
};
},
// Get consumption history for chart
getConsumptionHistory: async (period = '30days') => {
await new Promise(resolve => setTimeout(resolve, 600));
// Generate mock history data
const days = 30;
const history = [];
let cumulative = 0;
for (let i = days; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const daily = Math.floor(Math.random() * 5);
cumulative += daily;
history.push({
date: date.toISOString().split('T')[0],
daily,
cumulative
});
}
return history;
},
// List all signatures
getSignatures: async (filters = {}) => {
await new Promise(resolve => setTimeout(resolve, 600));
return [...mockSignatures].sort((a, b) => new Date(b.sentAt) - new Date(a.sentAt));
},
// Purchase credits
purchaseCredits: async (amount) => {
await new Promise(resolve => setTimeout(resolve, 1500));
creditBalance += amount;
totalPurchased += amount;
return { success: true, newBalance: creditBalance };
},
// Send reminder
sendReminder: async (signatureId) => {
await new Promise(resolve => setTimeout(resolve, 1000));
const sig = mockSignatures.find(s => s.id === signatureId);
if (sig) {
sig.remindersSent += 1;
return { success: true, remindersSent: sig.remindersSent };
}
return { success: false };
},
// Update settings
updateSettings: async (settings) => {
await new Promise(resolve => setTimeout(resolve, 1000));
return { success: true };
}
};

View file

@ -0,0 +1,10 @@
import type { RootState } from "@/store/store";
export const getAllArticles = (state: RootState) => state.article.articles;
export const getArticleSelected = (state: RootState) => state.article.articleSelected;
export const articleStatus = (state: RootState) => state.article.status;
export const articleError = (state: RootState) => state.article.error;

View file

@ -0,0 +1,162 @@
/* eslint-disable import/order */
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { articleState } from "./type";
import { addStock, createArticle, getarticleById, getArticles, updateArticle } from "./thunk";
import { Article } from "@/types/articleType";
const initialState: articleState = {
status: "idle",
error: null,
articles: [],
articleSelected: null,
};
const articleSlice = createSlice({
name: "article",
initialState,
reducers: {
selectArticle: (state, action: PayloadAction<Article>) => {
state.articleSelected = action.payload;
}
},
extraReducers: (builder) => {
/**
* get all articles
*/
builder.addCase(getArticles.fulfilled, (state, action) => {
const data = action.payload;
state.articles = data;
state.error = null;
state.status = "succeeded";
});
builder.addCase(getArticles.pending, (state) => {
state.status = "loading";
});
builder.addCase(getArticles.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* get article by id
*/
builder.addCase(getarticleById.pending, (state) => {
state.status = "loading";
});
builder.addCase(getarticleById.fulfilled, (state, action) => {
const data = action.payload as Article;
const idx = state.articles.findIndex((n) => n.reference === data.reference);
if (idx !== -1) {
state.articles[idx] = data;
} else {
state.articles.push(data);
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(getarticleById.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* add article
*/
builder.addCase(createArticle.fulfilled, (state, action) => {
const data = action.payload as any
state.articles.unshift(data);
state.status = "succeeded";
state.error = null;
});
builder.addCase(createArticle.pending, (state) => {
state.status = "loading";
});
builder.addCase(createArticle.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* update article
*/
builder.addCase(updateArticle.pending, (state) => {
state.status = "loading";
});
builder.addCase(updateArticle.fulfilled, (state, action) => {
const data = action.payload as Article
const idx = state.articles.findIndex((p) => p.reference === data.reference);
if (idx !== -1){
state.articles[idx] = data
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(updateArticle.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "Failed to update";
});
/**
* add stock
*/
builder.addCase(addStock.pending, (state) => {
state.status = "loading";
});
builder.addCase(addStock.fulfilled, (state, action) => {
const { data, quantite } = action.payload;
const idx = state.articles.findIndex(
(p) => p.reference === data.article_ref
);
if (idx !== -1) {
state.articles[idx].stock_reel += quantite;
state.articles[idx].stock_disponible! += quantite;
}
if (state.articleSelected) {
state.articleSelected.stock_reel += quantite;
state.articleSelected.stock_disponible! += quantite;
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(addStock.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "Failed to update";
});
},
});
export const { selectArticle } = articleSlice.actions
export default articleSlice.reducer;

View file

@ -0,0 +1,92 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable import/order */
import { sageService } from '@/service/sageService';
import { Article, ArticleRequest, ArticleUpdateRequest, StockRequest, StockResponse } from '@/types/articleType';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* get all artcles
*/
export const getArticles = createAsyncThunk(
'article/getArticles',
async (): Promise<Article[]> => {
try {
return await sageService.getArticles();
} catch (err) {
throw err;
}
}
);
/**
* get article by id
*/
export const getarticleById = createAsyncThunk(
'article/getarticle',
async (id: string): Promise<Article> => {
try {
return await sageService.getArticleId(id);
} catch (err) {
throw err;
}
}
);
/**
* add article
*/
export const createArticle = createAsyncThunk(
'article/createArticle',
async (data: ArticleRequest): Promise<Article> => {
try {
return await sageService.createArticle(data);
} catch (err) {
throw err;
}
}
);
/**
* update article
*/
export const updateArticle = createAsyncThunk(
'article/updateArticle',
async (
{ reference, data }: { reference: string; data: ArticleUpdateRequest }
): Promise<Article> => {
try {
return await sageService.updateArticle(reference, data);
} catch (err) {
throw err;
}
}
);
/**
* add stock
*/
export const addStock = createAsyncThunk(
'article/addStock',
async (data: StockRequest) => {
try {
const res = await sageService.addStock(data);
return {
data: res,
quantite: data.lignes[0].quantite
}
} catch (err) {
throw err;
}
}
);

View file

@ -0,0 +1,10 @@
import { Article } from "@/types/articleType";
export interface articleState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
articles: Article[];
articleSelected: Article | null;
}

View file

@ -0,0 +1,10 @@
import type { RootState } from "@/store/store";
export const getAllavoir = (state: RootState) => state.avoir.avoirs;
export const getavoirSelected = (state: RootState) => state.avoir.avoirSelected;
export const avoirStatus = (state: RootState) => state.avoir.status;
export const avoirError = (state: RootState) => state.avoir.error;

View file

@ -0,0 +1,133 @@
/* eslint-disaAvoire import/order */
/* eslint-disaAvoire @typescript-eslint/consistent-type-imports */
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { avoirState } from "./type";
import { Avoir } from "@/types/avoirType";
import { createAvoir, getAvoir, getAvoirs, selectAvoirAsync, updateAvoir } from "./thunk";
const initialState: avoirState = {
status: "idle",
error: null,
avoirs: [],
avoirSelected: null,
};
const avoirSlice = createSlice({
name: "avoir",
initialState,
reducers: {
selectavoir: (state, action: PayloadAction<Avoir>) => {
state.avoirSelected = action.payload;
}
},
extraReducers: (builder) => {
/**
* get all avoir
*/
builder.addCase(getAvoirs.fulfilled, (state, action) => {
const data = action.payload;
state.avoirs = data;
state.error = null;
state.status = "succeeded";
});
builder.addCase(getAvoirs.pending, (state) => {
state.status = "loading";
});
builder.addCase(getAvoirs.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* get avoir by id
*/
builder.addCase(getAvoir.pending, (state) => {
state.status = "loading";
});
builder.addCase(getAvoir.fulfilled, (state, action) => {
const data = action.payload as Avoir;
const idx = state.avoirs.findIndex((n) => n.numero === data.numero);
if (idx !== -1) {
state.avoirs[idx] = data;
} else {
state.avoirs.push(data);
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(getAvoir.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* create Avoir
*/
builder.addCase(createAvoir.pending, (state) => {
state.status = "loading";
});
builder.addCase(createAvoir.fulfilled, (state) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(createAvoir.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* update Avoir
*/
builder.addCase(updateAvoir.pending, (state) => {
state.status = "loading";
});
builder.addCase(updateAvoir.fulfilled, (state) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(updateAvoir.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* verified Avoir
*/
builder.addCase(selectAvoirAsync.fulfilled, (state, action) => {
state.avoirSelected = {
...action.payload
};
const idx = state.avoirs.findIndex(d => d.numero === action.payload.numero);
if (idx !== -1) {
state.avoirs[idx] = state.avoirSelected;
} else {
state.avoirs.push(state.avoirSelected);
}
});
},
});
export const { selectavoir } = avoirSlice.actions
export default avoirSlice.reducer;

View file

@ -0,0 +1,94 @@
/* eslint-disaAvoire import/no-unresolved */
/* eslint-disaAvoire import/order */
import { sageService } from '@/service/sageService';
import { Avoir, AvoirRequest, AvoirUpdateRequest } from '@/types/avoirType';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* get all avoir
*/
export const getAvoirs = createAsyncThunk(
'avoir/getAvoirs',
async (): Promise<Avoir[]> => {
try {
return await sageService.getAvoirs();
} catch (err) {
throw err;
}
}
);
/**
* get avoir by id
*/
export const getAvoir = createAsyncThunk(
'avoir/getAvoir',
async (id: string): Promise<Avoir> => {
try {
return await sageService.getAvoir(id);
} catch (err) {
throw err;
}
}
);
/**
* create Avoir
*/
export const createAvoir = createAsyncThunk(
'avoir/createAvoir',
async (data: AvoirRequest) => {
try {
return await sageService.createAvoir(data);
} catch (err) {
throw err;
}
}
);
/**
* update Avoir
*/
export const updateAvoir = createAsyncThunk(
'avoir/updateAvoir',
async (
{ numero, data }: { numero: string; data: AvoirUpdateRequest }
) => {
try {
return await sageService.updateAvoir(numero, data);
} catch (err) {
throw err;
}
}
);
/**
* verified Avoir
*/
export const selectAvoirAsync = createAsyncThunk<
Avoir,
Avoir,
{ state: any }
>(
"avoir/selectAvoirAsync",
async (items, { dispatch }) => {
if (!items.lignes) {
const updateItem = await dispatch(getAvoir(items.numero)).unwrap() as any
return updateItem;
}
return items;
}
);

View file

@ -0,0 +1,10 @@
import { Avoir } from "@/types/avoirType";
export interface avoirState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
avoirs: Avoir[];
avoirSelected: Avoir | null;
}

View file

@ -0,0 +1,10 @@
import type { RootState } from "@/store/store";
export const getAllBLs = (state: RootState) => state.BL.BLs;
export const getBLSelected = (state: RootState) => state.BL.BLSelected;
export const BLStatus = (state: RootState) => state.BL.status;
export const BLError = (state: RootState) => state.BL.error;

View file

@ -0,0 +1,197 @@
/* eslint-disable import/order */
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { BLState } from "./type";
import { BL } from "@/types/BL_Types";
import { blToFacture, changerStatutBL, createBL, getAllBL, getBL, selectBLAsync, updateBL } from "./thunk";
import { StatusResponse, TransformationResponse } from "@/types/sageTypes";
const initialState: BLState = {
status: "idle",
error: null,
BLs: [],
BLSelected: null,
};
const BLSlice = createSlice({
name: "BL",
initialState,
reducers: {
selectBL: (state, action: PayloadAction<BL>) => {
state.BLSelected = action.payload;
}
},
extraReducers: (builder) => {
/**
* get all BLs
*/
builder.addCase(getAllBL.fulfilled, (state, action) => {
const data = action.payload;
state.BLs = data;
state.error = null;
state.status = "succeeded";
});
builder.addCase(getAllBL.pending, (state) => {
state.status = "loading";
});
builder.addCase(getAllBL.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* get BL by id
*/
builder.addCase(getBL.pending, (state) => {
state.status = "loading";
});
builder.addCase(getBL.fulfilled, (state, action) => {
const data = action.payload as BL;
const idx = state.BLs.findIndex((n) => n.numero === data.numero);
if (idx !== -1) {
state.BLs[idx] = data;
} else {
state.BLs.push(data);
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(getBL.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* create BL
*/
builder.addCase(createBL.pending, (state) => {
state.status = "loading";
});
builder.addCase(createBL.fulfilled, (state) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(createBL.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* update BL
*/
builder.addCase(updateBL.pending, (state) => {
state.status = "loading";
});
builder.addCase(updateBL.fulfilled, (state) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(updateBL.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* verified bg
*/
builder.addCase(selectBLAsync.fulfilled, (state, action) => {
state.BLSelected = {
...action.payload
};
const idx = state.BLs.findIndex(d => d.numero === action.payload.numero);
if (idx !== -1) {
state.BLs[idx] = state.BLSelected;
} else {
state.BLs.push(state.BLSelected);
}
});
/**
* BL to facture
*/
builder.addCase(blToFacture.pending, (state) => {
state.status = "loading";
});
builder.addCase(blToFacture.fulfilled, (state, action) => {
const data = action.payload as TransformationResponse;
const idx = state.BLs.findIndex((n) => n.numero === data.document_source);
if (idx !== -1) {
state.BLs.splice(idx, 1)
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(blToFacture.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* update BL status
*/
builder.addCase(changerStatutBL.pending, (state) => {
state.status = "loading";
});
builder.addCase(changerStatutBL.fulfilled, (state, action) => {
const data = action.payload as StatusResponse;
const idx = state.BLs.findIndex((n) => n.numero === data.document_id);
if (idx !== -1) {
const oldItem = state.BLs[idx];
state.BLs[idx] = {
...oldItem,
statut: data.statut_nouveau
};
}
if (state.BLSelected?.numero === data.document_id) {
const oldItem = state.BLSelected;
state.BLSelected= {
...oldItem,
statut: data.statut_nouveau
};
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(changerStatutBL.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
},
});
export const { selectBL } = BLSlice.actions
export default BLSlice.reducer;

View file

@ -0,0 +1,128 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable import/order */
import { sageService } from '@/service/sageService';
import { BL, BLRequest, BLUpdateRequest } from '@/types/BL_Types';
import { StatusRequest, StatusResponse, TransformationResponse } from '@/types/sageTypes';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* get all BLs
*/
export const getAllBL = createAsyncThunk(
'BL/getAllBL',
async (): Promise<BL[]> => {
try {
return await sageService.getAllBL();
} catch (err) {
throw err;
}
}
);
/**
* get BL by id
*/
export const getBL = createAsyncThunk(
'BL/getBL',
async (id: string): Promise<BL> => {
try {
return await sageService.getBL(id);
} catch (err) {
throw err;
}
}
);
/**
* changed statut BL
*/
export const changerStatutBL = createAsyncThunk(
'BL/changerStatutBL',
async (data: StatusRequest): Promise<StatusResponse> => {
try {
return await sageService.changerStatut(30, data);
} catch (err) {
throw err;
}
}
);
/**
* create BL
*/
export const createBL = createAsyncThunk(
'BL/createBL',
async (data: BLRequest) => {
try {
return await sageService.createBL(data);
} catch (err) {
throw err;
}
}
);
/**
* update BL
*/
export const updateBL = createAsyncThunk(
'BL/updateBL',
async (
{ numero, data }: { numero: string; data: BLUpdateRequest }
) => {
try {
return await sageService.updateBL(numero, data);
} catch (err) {
throw err;
}
}
);
/**
* verified BLs
*/
export const selectBLAsync = createAsyncThunk<
BL,
BL,
{ state: any }
>(
"BL/selectBLAsync",
async (items, { dispatch }) => {
if (!items.lignes) {
const updateItem = await dispatch(getBL(items.numero)).unwrap() as any
return updateItem;
}
return items;
}
);
/**
* change bl to facture
*/
export const blToFacture = createAsyncThunk(
'BL/blToFacture',
async (numero: string): Promise<TransformationResponse> => {
try {
return await sageService.blToFacture(numero);
} catch (err) {
throw err;
}
}
);

View file

@ -0,0 +1,9 @@
import { BL } from "@/types/BL_Types";
export interface BLState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
BLs: BL[];
BLSelected: BL | null;}

View file

@ -0,0 +1,9 @@
import { RootState } from "@/store/store";
export const getAllClients = (state: RootState) => state.client.clients;
export const getClientSelected = (state: RootState) => state.client.clientSelected;
export const clientStatus = (state: RootState) => state.client.status;
export const clientError = (state: RootState) => state.client.error;

View file

@ -0,0 +1,259 @@
/* eslint-disable import/order */
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { clientState } from "./type";
import { addClient, getClient, getClients, updateClient, updateStatus } from "./thunk";
import { Client, Contacts } from "@/types/clientType";
import { addContactClient, updateContactClient } from "../contact/thunk";
const initialState: clientState = {
status: "idle",
error: null,
clients: [],
clientSelected: null,
};
const clientSlice = createSlice({
name: "client",
initialState,
reducers: {
selectClient: (state, action: PayloadAction<Client>) => {
state.clientSelected = action.payload;
}
},
extraReducers: (builder) => {
/**
* get all clients
*/
builder.addCase(getClients.fulfilled, (state, action) => {
const data = action.payload;
state.clients = data;
state.error = null;
state.status = "succeeded";
});
builder.addCase(getClients.pending, (state) => {
state.status = "loading";
});
builder.addCase(getClients.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* get client by id
*/
builder.addCase(getClient.pending, (state) => {
state.status = "loading";
});
builder.addCase(getClient.fulfilled, (state, action) => {
const payload = action.payload as Client;
const idx = state.clients.findIndex((n) => n.numero === payload.numero);
if (idx !== -1) {
state.clients[idx] = payload;
} else {
state.clients.unshift(payload);
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(getClient.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* create contact client
*/
builder.addCase(addContactClient.pending, (state) => {
state.status = "loading";
});
builder.addCase(addContactClient.fulfilled, (state, action) => {
const payload = action.payload as Contacts;
const idx = state.clients.findIndex((n) => n.numero === payload.numero);
if (idx !== -1) {
state.clients[idx].contacts?.unshift(payload);
}
if (state.clientSelected?.numero === payload.numero) {
state.clientSelected.contacts?.unshift(payload)
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(addContactClient.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* update contact client
*/
builder.addCase(updateContactClient.pending, (state) => {
state.status = "loading";
});
builder.addCase(updateContactClient.fulfilled, (state, action) => {
const payload = action.payload as Contacts;
const idx = state.clients.findIndex((n) => n.numero === payload.numero);
if (idx !== -1) {
const idxContact = state.clients[idx].contacts?.findIndex((n) => n.contact_numero === payload.contact_numero);
if (idxContact !== -1) {
state.clients[idx].contacts![idxContact || 0] = payload
}
}
if (state.clientSelected?.numero === payload.numero) {
const idxContact = state.clientSelected.contacts?.findIndex((n) => n.contact_numero === payload.contact_numero);
if (idxContact !== -1) {
state.clientSelected.contacts![idxContact || 0] = payload
}
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(updateContactClient.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* add client
*/
builder.addCase(addClient.fulfilled, (state, action) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(addClient.pending, (state) => {
state.status = "loading";
});
builder.addCase(addClient.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* update client
*/
builder.addCase(updateClient.pending, (state) => {
state.status = "loading";
});
builder.addCase(updateClient.fulfilled, (state, action) => {
const { client, success } = action.payload as unknown as { success: boolean; client: Client };
if (success && client?.numero) {
const idx = state.clients.findIndex((p) => p.numero === client.numero);
if (idx !== -1) {
const oldItem = state.clients[idx];
state.clients[idx] = {
...oldItem,
intitule: client.intitule,
adresse: client.adresse,
code_postal: client.code_postal,
ville: client.ville,
telephone: client.telephone,
email: client.email,
siret: client.siret,
tva_intra: client.tva_intra,
code_naf: client.code_naf,
encours_autorise: client.encours_autorise,
assurance_credit: client.assurance_credit,
pays: client.pays,
est_actif: client.est_actif,
est_prospect: client.est_prospect,
type_tiers: client.type_tiers
};
}
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(updateClient.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "Failed to update";
});
/**
* update status client
*/
builder.addCase(updateStatus.pending, (state) => {
state.status = "loading";
});
builder.addCase(updateStatus.fulfilled, (state, action) => {
const { client, success } = action.payload as unknown as { success: boolean; client: Client };
if (success && client?.numero) {
const idx = state.clients.findIndex((p) => p.numero === client.numero);
if (idx !== -1) {
const oldItem = state.clients[idx];
state.clients[idx] = {
...oldItem,
intitule: client.intitule,
adresse: client.adresse,
code_postal: client.code_postal,
ville: client.ville,
telephone: client.telephone,
email: client.email,
siret: client.siret,
tva_intra: client.tva_intra,
code_naf: client.code_naf,
encours_autorise: client.encours_autorise,
assurance_credit: client.assurance_credit,
pays: client.pays,
est_actif: client.est_actif,
est_prospect: client.est_prospect,
type_tiers: client.type_tiers
};
}
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(updateStatus.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "Failed to update";
});
},
});
export const { selectClient } = clientSlice.actions
export default clientSlice.reducer;

View file

@ -0,0 +1,90 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable import/order */
import { sageService } from '@/service/sageService';
import { Client, ClientCreateResponse, ClientRequest, ClientUpdateRequest } from '@/types/clientType';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* get all clients
*/
export const getClients = createAsyncThunk(
'client/getClients',
async (): Promise<Client[]> => {
try {
return await sageService.getClients();
} catch (err) {
throw err;
}
}
);
/**
* get client by id
*/
export const getClient = createAsyncThunk(
'client/getClient',
async (id: string): Promise<Client> => {
try {
return await sageService.getClient(id);
} catch (err) {
throw err;
}
}
);
/**
* add client
*/
export const addClient = createAsyncThunk(
'client/addClient',
async (data: ClientRequest): Promise<ClientCreateResponse> => {
try {
return await sageService.addClient(data);
} catch (err) {
throw err;
}
}
);
/**
* update client
*/
export const updateClient = createAsyncThunk(
'client/updateClient',
async (
{ numero, data }: { numero: string; data: ClientUpdateRequest }
): Promise<Client> => {
try {
return await sageService.updateClient(numero, data);
} catch (err) {
throw err;
}
}
);
/**
* update client status
*/
export const updateStatus = createAsyncThunk(
'client/updateStatus',
async (
{ numero, est_actif }: { numero: string; est_actif: boolean }
): Promise<Client> => {
try {
return await sageService.updateStatusClient(numero, est_actif);
} catch (err) {
throw err;
}
}
);

View file

@ -0,0 +1,11 @@
import { Client } from "@/types/clientType";
export interface clientState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
clients: Client[];
clientSelected: Client | null;
}

View file

@ -0,0 +1,10 @@
import type { RootState } from "@/store/store";
export const getAllcommandes = (state: RootState) => state.commande.commandes;
export const getcommandeSelected = (state: RootState) => state.commande.commandeSelected;
export const commandeStatus = (state: RootState) => state.commande.status;
export const commandeError = (state: RootState) => state.commande.error;

View file

@ -0,0 +1,222 @@
/* eslint-disable import/order */
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { commandeState } from "./type";
import { changerStatutCommande, commandeToBL, commandeToFacture, createCommande, getCommande, getCommandes, selectCommandeAsync, updateCommande } from "./thunk";
import { Commande } from "@/types/commandeTypes";
import { StatusResponse, TransformationResponse } from "@/types/sageTypes";
const initialState: commandeState = {
status: "idle",
error: null,
commandes: [],
commandeSelected: null,
};
const commandeSlice = createSlice({
name: "commande",
initialState,
reducers: {
selectcommande: (state, action: PayloadAction<Commande>) => {
state.commandeSelected = action.payload;
}
},
extraReducers: (builder) => {
/**
* get all commandes
*/
builder.addCase(getCommandes.fulfilled, (state, action) => {
const data = action.payload;
state.commandes = data;
state.error = null;
state.status = "succeeded";
});
builder.addCase(getCommandes.pending, (state) => {
state.status = "loading";
});
builder.addCase(getCommandes.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* get commande by id
*/
builder.addCase(getCommande.pending, (state) => {
state.status = "loading";
});
builder.addCase(getCommande.fulfilled, (state, action) => {
const data = action.payload as Commande;
const idx = state.commandes.findIndex((n) => n.numero === data.numero);
if (idx !== -1) {
state.commandes[idx] = data;
} else {
state.commandes.push(data);
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(getCommande.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* create commande
*/
builder.addCase(createCommande.pending, (state) => {
state.status = "loading";
});
builder.addCase(createCommande.fulfilled, (state) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(createCommande.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* update commande
*/
builder.addCase(updateCommande.pending, (state) => {
state.status = "loading";
});
builder.addCase(updateCommande.fulfilled, (state) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(updateCommande.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* verified commande
*/
builder.addCase(selectCommandeAsync.fulfilled, (state, action) => {
state.commandeSelected = {
...action.payload
};
const idx = state.commandes.findIndex(d => d.numero === action.payload.numero);
if (idx !== -1) {
state.commandes[idx] = state.commandeSelected;
} else {
state.commandes.push(state.commandeSelected);
}
});
/**
* devis to commande
*/
builder.addCase(commandeToBL.pending, (state) => {
state.status = "loading";
});
builder.addCase(commandeToBL.fulfilled, (state, action) => {
const data = action.payload as TransformationResponse;
const idx = state.commandes.findIndex((n) => n.numero === data.document_source);
if (idx !== -1) {
state.commandes.splice(idx, 1)
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(commandeToBL.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* devis to facture
*/
builder.addCase(commandeToFacture.pending, (state) => {
state.status = "loading";
});
builder.addCase(commandeToFacture.fulfilled, (state, action) => {
const data = action.payload as TransformationResponse;
const idx = state.commandes.findIndex((n) => n.numero === data.document_source);
if (idx !== -1) {
state.commandes.splice(idx, 1)
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(commandeToFacture.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* update commande status
*/
builder.addCase(changerStatutCommande.pending, (state) => {
state.status = "loading";
});
builder.addCase(changerStatutCommande.fulfilled, (state, action) => {
const data = action.payload as StatusResponse;
const idx = state.commandes.findIndex((n) => n.numero === data.document_id);
if (idx !== -1) {
const oldItem = state.commandes[idx];
state.commandes[idx] = {
...oldItem,
statut: data.statut_nouveau
};
}
if (state.commandeSelected?.numero === data.document_id) {
const oldItem = state.commandeSelected;
state.commandeSelected= {
...oldItem,
statut: data.statut_nouveau
};
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(changerStatutCommande.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
},
});
export const { selectcommande } = commandeSlice.actions
export default commandeSlice.reducer;

View file

@ -0,0 +1,140 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable import/order */
import { sageService } from '@/service/sageService';
import { Commande, CommandeRequest, CommandeResponse, CommandeUpdateRequest } from '@/types/commandeTypes';
import { StatusRequest, StatusResponse, TransformationResponse } from '@/types/sageTypes';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* get all commandes
*/
export const getCommandes = createAsyncThunk(
'commande/getCommandes',
async (): Promise<Commande[]> => {
try {
return await sageService.getCommandes();
} catch (err) {
throw err;
}
}
);
/**
* get commande by id
*/
export const getCommande = createAsyncThunk(
'commande/getCommande',
async (id: string): Promise<Commande> => {
try {
return await sageService.getCommande(id);
} catch (err) {
throw err;
}
}
);
/**
* changed statut commande
*/
export const changerStatutCommande = createAsyncThunk(
'commande/changerStatutCommande',
async (data: StatusRequest): Promise<StatusResponse> => {
try {
return await sageService.changerStatut(10, data);
} catch (err) {
throw err;
}
}
);
/**
* commande to facture
*/
export const commandeToFacture = createAsyncThunk(
'commande/commandeToFacture',
async (id: string): Promise<TransformationResponse> => {
try {
return await sageService.commandeToFacture(id);
} catch (err) {
throw err;
}
}
);
/**
* commande to BL
*/
export const commandeToBL = createAsyncThunk(
'commande/commandeToBL',
async (id: string): Promise<TransformationResponse> => {
try {
return await sageService.commandeToBL(id);
} catch (err) {
throw err;
}
}
);
/**
* create commande
*/
export const createCommande = createAsyncThunk(
'commande/createCommande',
async (data: CommandeRequest): Promise<CommandeResponse> => {
try {
return await sageService.createCommande(data);
} catch (err) {
throw err;
}
}
);
/**
* update commande
*/
export const updateCommande = createAsyncThunk(
'commande/updateCommande',
async (
{ numero, data }: { numero: string; data: CommandeUpdateRequest }
) => {
try {
return await sageService.updateCommande(numero, data);
} catch (err) {
throw err;
}
}
);
/**
* verified commandes
*/
export const selectCommandeAsync = createAsyncThunk<
Commande,
Commande,
{ state: any }
>(
"commande/selectCommandeAsync",
async (devis, { dispatch }) => {
if (!devis.lignes) {
const updateItem = await dispatch(getCommande(devis.numero)).unwrap() as any
return updateItem;
}
return devis;
}
);

View file

@ -0,0 +1,9 @@
import { Commande } from "@/types/commandeTypes";
export interface commandeState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
commandes: Commande[];
commandeSelected: Commande | null;}

View file

@ -0,0 +1,10 @@
import type { RootState } from "@/store/store";
export const getAllcommercials = (state: RootState) => state.commercials.commercials;
export const getcommercialsSelected = (state: RootState) => state.commercials.commercialsSelected;
export const commercialsStatus = (state: RootState) => state.commercials.status;
export const commercialsError = (state: RootState) => state.commercials.error;

View file

@ -0,0 +1,80 @@
/* eslint-disable import/order */
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { commercialsState } from "./type";
import { Commercial } from "@/types/commercialType";
import { getCommercials, getcommercialsById } from "./thunk";
const initialState: commercialsState = {
status: "idle",
error: null,
commercials: [],
commercialsSelected: null,
};
const commercialsSlice = createSlice({
name: "commercials",
initialState,
reducers: {
selectcommercials: (state, action: PayloadAction<Commercial>) => {
state.commercialsSelected = action.payload;
}
},
extraReducers: (builder) => {
/**
* get all commercials
*/
builder.addCase(getCommercials.fulfilled, (state, action) => {
const data = action.payload;
state.commercials = data;
state.error = null;
state.status = "succeeded";
});
builder.addCase(getCommercials.pending, (state) => {
state.status = "loading";
});
builder.addCase(getCommercials.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* get commercials by id
*/
builder.addCase(getcommercialsById.pending, (state) => {
state.status = "loading";
});
builder.addCase(getcommercialsById.fulfilled, (state, action) => {
const data = action.payload as Commercial;
const idx = state.commercials.findIndex((n) => n.numero === data.numero);
if (idx !== -1) {
state.commercials[idx] = data;
} else {
state.commercials.push(data);
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(getcommercialsById.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
},
});
export const { selectcommercials } = commercialsSlice.actions
export default commercialsSlice.reducer;

View file

@ -0,0 +1,73 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable import/order */
import { sageService } from '@/service/sageService';
import { Commercial, CommercialRequest } from '@/types/commercialType';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* get all commercial
*/
export const getCommercials = createAsyncThunk(
'commercials/getCommercials',
async (): Promise<Commercial[]> => {
try {
return await sageService.commercials();
} catch (err) {
throw err;
}
}
);
/**
* get commercials by id
*/
export const getcommercialsById = createAsyncThunk(
'commercials/getcommercialsById',
async (id: string): Promise<Commercial> => {
try {
return await sageService.commercialsById(id);
} catch (err) {
throw err;
}
}
);
/**
* add commercials
*/
export const addCommercial = createAsyncThunk(
'commercials/addCommercial',
async (data: CommercialRequest): Promise<Commercial> => {
try {
return await sageService.addCommercial(data);
} catch (err) {
throw err;
}
}
);
/**
* update commercials
*/
// export const updatecommercials = createAsyncThunk(
// 'commercials/updatecommercials',
// async (
// { reference, data }: { reference: string; data: commercialsUpdateRequest }
// ): Promise<commercials> => {
// try {
// return await sageService.updatecommercials(reference, data);
// } catch (err) {
// throw err;
// }
// }
// );

View file

@ -0,0 +1,10 @@
import { Commercial } from "@/types/commercialType";
export interface commercialsState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
commercials: Commercial[];
commercialsSelected: Commercial | null;
}

View file

@ -0,0 +1,10 @@
import type { RootState } from "@/store/store";
export const getAllcontacts = (state: RootState) => state.contact.contacts;
export const getcontactSelected = (state: RootState) => state.contact.contactSelected;
export const contactStatus = (state: RootState) => state.contact.status;
export const contactError = (state: RootState) => state.contact.error;

View file

@ -0,0 +1,100 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { contactState } from "./type";
import { Contacts } from "@/types/clientType";
import { addContactClient, addContactFournisseur, updateContactClient, updateContactFournisseur } from "./thunk";
const initialState: contactState = {
status: "idle",
error: null,
contacts: [],
contactSelected: null,
};
const contactSlice = createSlice({
name: "contact",
initialState,
reducers: {
selectcontact: (state, action: PayloadAction<Contacts>) => {
state.contactSelected = action.payload;
}
},
extraReducers: (builder) => {
/**
* create contact client
*/
builder.addCase(addContactClient.pending, (state) => {
state.status = "loading";
});
builder.addCase(addContactClient.fulfilled, (state, action) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(addContactClient.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* update contact client
*/
builder.addCase(updateContactClient.pending, (state) => {
state.status = "loading";
});
builder.addCase(updateContactClient.fulfilled, (state, action) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(updateContactClient.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* create contact fournisseur
*/
builder.addCase(addContactFournisseur.pending, (state) => {
state.status = "loading";
});
builder.addCase(addContactFournisseur.fulfilled, (state, action) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(addContactFournisseur.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* update contact fournisseur
*/
builder.addCase(updateContactFournisseur.pending, (state) => {
state.status = "loading";
});
builder.addCase(updateContactFournisseur.fulfilled, (state, action) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(updateContactFournisseur.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
}
});
export const { selectcontact } = contactSlice.actions
export default contactSlice.reducer;

View file

@ -0,0 +1,94 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable import/order */
import { sageService } from '@/service/sageService';
import { ContactRequest, Contacts } from '@/types/clientType';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* get all contacts clients
*/
export const contactsClient = createAsyncThunk(
'contact/contactsClient',
async (numero: string): Promise<Contacts[]> => {
try {
return await sageService.contacts(numero);
} catch (err) {
throw err;
}
}
);
/**
* add contact
*/
export const addContactClient = createAsyncThunk(
'contact/addContactClient',
async (data: ContactRequest): Promise<Contacts> => {
try {
return await sageService.addContact(data);
} catch (err) {
throw err;
}
}
);
/**
* update contact
*/
export const updateContactClient = createAsyncThunk(
'contact/updateContactClient',
async (
{
contact_numero,
data,
}: { contact_numero: number; data: ContactRequest }
): Promise<Contacts> => {
return await sageService.updateContact(contact_numero, data);
}
);
/**
* add contact fournisseur
*/
export const addContactFournisseur = createAsyncThunk(
'contact/addContactFournisseur',
async (data: ContactRequest): Promise<Contacts> => {
try {
return await sageService.addContact(data);
} catch (err) {
throw err;
}
}
);
/**
* update contact fournisseur
*/
export const updateContactFournisseur = createAsyncThunk(
'contact/updateContactFournisseur',
async (
{
contact_numero,
data,
}: { contact_numero: number; data: ContactRequest }
): Promise<Contacts> => {
return await sageService.updateContact(contact_numero, data);
}
);

View file

@ -0,0 +1,10 @@
import { Contacts } from "@/types/clientType";
export interface contactState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
contacts: Contacts[];
contactSelected: Contacts | null;
}

View file

@ -0,0 +1,10 @@
import type { RootState } from "@/store/store";
export const getAllDevis = (state: RootState) => state.devis.devis;
export const getDevisSelected = (state: RootState) => state.devis.devisSelected;
export const devisStatus = (state: RootState) => state.devis.status;
export const devisError = (state: RootState) => state.devis.error;

View file

@ -0,0 +1,230 @@
/* eslint-disable import/order */
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { devisState } from "./type";
import { changerStatutDevis, createDevis, devisToCommande, devisToFacture, getDevisById, getDevisList, selectDevisAsync, updateDevis } from "./thunk";
import { DevisListItem, DevisStatusResponse } from "@/types/devisType";
import { StatusResponse, TransformationResponse } from "@/types/sageTypes";
const initialState: devisState = {
status: "idle",
error: null,
devis: [],
devisSelected: null,
};
const devisSlice = createSlice({
name: "devis",
initialState,
reducers: {
selectDevis: (state, action: PayloadAction<DevisListItem>) => {
state.devisSelected = action.payload;
}
},
extraReducers: (builder) => {
/**
* get all devis
*/
builder.addCase(getDevisList.fulfilled, (state, action) => {
const data = action.payload.map((d: DevisListItem) => ({
...d,
a_deja_ete_transforme: d.a_deja_ete_transforme ?? false,
isVerified: d.isVerified ?? false,
}));
state.devis = data;
state.error = null;
state.status = "succeeded";
});
builder.addCase(getDevisList.pending, (state) => {
state.status = "loading";
});
builder.addCase(getDevisList.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* get devis by id
*/
builder.addCase(getDevisById.pending, (state) => {
state.status = "loading";
});
builder.addCase(getDevisById.fulfilled, (state, action) => {
const payload = action.payload as any;
const data = payload.data as DevisListItem
const idx = state.devis.findIndex((n) => n.numero === data.numero);
if (idx !== -1) {
state.devis[idx] = data;
} else {
state.devis.push(data);
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(getDevisById.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* verified status devis
*/
builder.addCase(selectDevisAsync.fulfilled, (state, action) => {
state.devisSelected = {
...action.payload,
isVerified: true
};
const idx = state.devis.findIndex(d => d.numero === action.payload.numero);
if (idx !== -1) {
state.devis[idx] = state.devisSelected;
} else {
state.devis.push(state.devisSelected);
}
});
/**
* create devis
*/
builder.addCase(createDevis.pending, (state) => {
state.status = "loading";
});
builder.addCase(createDevis.fulfilled, (state) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(createDevis.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* update devis
*/
builder.addCase(updateDevis.pending, (state) => {
state.status = "loading";
});
builder.addCase(updateDevis.fulfilled, (state) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(updateDevis.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* update devis status
*/
builder.addCase(changerStatutDevis.pending, (state) => {
state.status = "loading";
});
builder.addCase(changerStatutDevis.fulfilled, (state, action) => {
const data = action.payload as StatusResponse;
const idx = state.devis.findIndex((n) => n.numero === data.document_id);
if (idx !== -1) {
const oldItem = state.devis[idx];
state.devis[idx] = {
...oldItem,
statut: data.statut_nouveau
};
}
if (state.devisSelected?.numero === data.document_id) {
const oldItem = state.devisSelected;
state.devisSelected= {
...oldItem,
statut: data.statut_nouveau
};
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(changerStatutDevis.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* devis to commande
*/
builder.addCase(devisToCommande.pending, (state) => {
state.status = "loading";
});
builder.addCase(devisToCommande.fulfilled, (state, action) => {
const data = action.payload as TransformationResponse;
const idx = state.devis.findIndex((n) => n.numero === data.document_source);
if (idx !== -1) {
state.devis.splice(idx, 1)
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(devisToCommande.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* devis to facture
*/
builder.addCase(devisToFacture.pending, (state) => {
state.status = "loading";
});
builder.addCase(devisToFacture.fulfilled, (state, action) => {
const data = action.payload as TransformationResponse;
const idx = state.devis.findIndex((n) => n.numero === data.document_source);
if (idx !== -1) {
state.devis.splice(idx, 1)
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(devisToFacture.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
},
});
export const { selectDevis } = devisSlice.actions
export default devisSlice.reducer;

View file

@ -0,0 +1,158 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable import/order */
import { sageService } from '@/service/sageService';
import { DevisListItem, DevisRequest, DevisResponse, DevisStatusRequest, DevisStatusResponse, DevisUpdateRequest } from '@/types/devisType';
import type { StatusRequest, StatusResponse, TransformationResponse } from '@/types/sageTypes';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* get all devis
*/
export const getDevisList = createAsyncThunk(
'devis/getDevisList',
async (): Promise<DevisListItem[]> => {
try {
return await sageService.getDevisList();
} catch (err) {
throw err;
}
}
);
/**
* get devis by id
*/
export const getDevisById = createAsyncThunk(
'devis/getDevis',
async (id: string): Promise<DevisListItem> => {
try {
return await sageService.getDevis(id);
} catch (err) {
throw err;
}
}
);
/**
* verified status devis
*/
export const selectDevisAsync = createAsyncThunk<
DevisListItem,
DevisListItem,
{ state: any }
>(
"devis/selectDevisAsync",
async (devis, { dispatch }) => {
if (!devis.isVerified && !devis.a_deja_ete_transforme) {
const updatedDevis = await dispatch(getDevisById(devis.numero)).unwrap() as any
return updatedDevis.data;
}
return devis;
}
);
/**
* create devis
*/
export const createDevis = createAsyncThunk(
'devis/createDevis',
async (data: DevisRequest): Promise<DevisResponse> => {
try {
return await sageService.createDevis(data);
} catch (err) {
throw err;
}
}
);
/**
* update devis
*/
export const updateDevis = createAsyncThunk(
'devis/updateDevis',
async (
{ numero, data }: { numero: string; data: DevisUpdateRequest }
) => {
try {
return await sageService.updateDevis(numero, data);
} catch (err) {
throw err;
}
}
);
/**
* changed statut devis
*/
export const changerStatutDevis = createAsyncThunk(
'devis/changerStatutDevis',
async (data: StatusRequest): Promise<StatusResponse> => {
try {
return await sageService.changerStatut(0, data);
} catch (err) {
throw err;
}
}
);
/**
* change devis to commande
*/
export const devisToCommande = createAsyncThunk(
'devis/devisToCommande',
async (numero: string): Promise<TransformationResponse> => {
try {
return await sageService.devisToCommande(numero);
} catch (err) {
throw err;
}
}
);
/**
* change devis to facture
*/
export const devisToFacture = createAsyncThunk(
'devis/devisToFacture',
async (numero: string): Promise<TransformationResponse> => {
try {
return await sageService.devisToFacture(numero);
} catch (err) {
throw err;
}
}
);
/**
* devis to pdf
*/
export const devisToPDF = createAsyncThunk(
'devis/devisToPDF',
async (numero: string) => {
try {
return await sageService.devisToPDF(numero);
} catch (err) {
throw err;
}
}
);

View file

@ -0,0 +1,10 @@
import { DevisListItem } from "@/types/devisType";
export interface devisState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
devis: DevisListItem[];
devisSelected: DevisListItem | null;
}

View file

@ -0,0 +1,8 @@
import { RootState } from "@/store/store";
export const getentrepriseSelected = (state: RootState) => state.entreprise.entrepriseSelected;
export const entrepriseStatus = (state: RootState) => state.entreprise.status;
export const entrepriseError = (state: RootState) => state.entreprise.error;

View file

@ -0,0 +1,30 @@
/* eslint-disable import/order */
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { entrepriseState } from "./type";
import { Entreprise } from "@/types/entreprise";
const initialState: entrepriseState = {
status: "idle",
error: null,
entrepriseSelected: null,
};
const entrepriseSlice = createSlice({
name: "entreprise",
initialState,
reducers: {
selectentreprise: (state, action: PayloadAction<Entreprise>) => {
state.entrepriseSelected = action.payload;
},
deselectentreprise: (state, action: PayloadAction<null>) => {
state.entrepriseSelected = action.payload;
}
},
});
export const { selectentreprise, deselectentreprise } = entrepriseSlice.actions
export default entrepriseSlice.reducer;

View file

@ -0,0 +1,24 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable import/order */
import { sageService } from '@/service/sageService';
import { EntrepriseResponse } from '@/types/entreprise';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* search entreprise
*/
export const searchEntreprise = createAsyncThunk(
'entreprise/searchEntreprise',
async (query: string): Promise<EntrepriseResponse> => {
try {
return await sageService.searchEntreprise(query);
} catch (err) {
throw err;
}
}
);

View file

@ -0,0 +1,9 @@
import { Entreprise } from "@/types/entreprise";
export interface entrepriseState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
entrepriseSelected: Entreprise | null;
}

View file

@ -0,0 +1,10 @@
import type { RootState } from "@/store/store";
export const getAllfacture = (state: RootState) => state.facture.factures;
export const getfactureSelected = (state: RootState) => state.facture.factureSelected;
export const factureStatus = (state: RootState) => state.facture.status;
export const factureError = (state: RootState) => state.facture.error;

View file

@ -0,0 +1,195 @@
/* eslint-disaFacturee import/order */
/* eslint-disaFacturee @typescript-eslint/consistent-type-imports */
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { factureState } from "./type";
import { Facture } from "@/types/factureType";
import { changerStatutFacture, createFacture, getFacture, getFactures, selectFactureAsync, updateFacture, validerFacture } from "./thunk";
import { StatusResponse } from "@/types/sageTypes";
const initialState: factureState = {
status: "idle",
error: null,
factures: [],
factureSelected: null,
};
const factureSlice = createSlice({
name: "facture",
initialState,
reducers: {
selectfacture: (state, action: PayloadAction<Facture>) => {
state.factureSelected = action.payload;
}
},
extraReducers: (builder) => {
/**
* get all facture
*/
builder.addCase(getFactures.fulfilled, (state, action) => {
const data = action.payload;
state.factures = data;
state.error = null;
state.status = "succeeded";
});
builder.addCase(getFactures.pending, (state) => {
state.status = "loading";
});
builder.addCase(getFactures.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* get facture by id
*/
builder.addCase(getFacture.pending, (state) => {
state.status = "loading";
});
builder.addCase(getFacture.fulfilled, (state, action) => {
const data = action.payload as Facture;
const idx = state.factures.findIndex((n) => n.numero === data.numero);
if (idx !== -1) {
state.factures[idx] = data;
} else {
state.factures.push(data);
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(getFacture.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
})
/**
* create Facture
*/
builder.addCase(createFacture.pending, (state) => {
state.status = "loading";
});
builder.addCase(createFacture.fulfilled, (state) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(createFacture.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* update Facture
*/
builder.addCase(updateFacture.pending, (state) => {
state.status = "loading";
});
builder.addCase(updateFacture.fulfilled, (state) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(updateFacture.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* update Facture
*/
builder.addCase(validerFacture.pending, (state) => {
state.status = "loading";
});
builder.addCase(validerFacture.fulfilled, (state) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(validerFacture.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* verified bg
*/
builder.addCase(selectFactureAsync.fulfilled, (state, action) => {
state.factureSelected = {
...action.payload
};
const idx = state.factures.findIndex(d => d.numero === action.payload.numero);
if (idx !== -1) {
state.factures[idx] = state.factureSelected;
} else {
state.factures.push(state.factureSelected);
}
});
/**
* update facture status
*/
builder.addCase(changerStatutFacture.pending, (state) => {
state.status = "loading";
});
builder.addCase(changerStatutFacture.fulfilled, (state, action) => {
const data = action.payload as StatusResponse;
const idx = state.factures.findIndex((n) => n.numero === data.document_id);
if (idx !== -1) {
const oldItem = state.factures[idx];
state.factures[idx] = {
...oldItem,
statut: data.statut_nouveau
};
}
if (state.factureSelected?.numero === data.document_id) {
const oldItem = state.factureSelected;
state.factureSelected= {
...oldItem,
statut: data.statut_nouveau
};
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(changerStatutFacture.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
},
});
export const { selectfacture } = factureSlice.actions
export default factureSlice.reducer;

View file

@ -0,0 +1,146 @@
/* eslint-disaFacturee import/no-unresolved */
/* eslint-disaFacturee import/order */
import { sageService } from '@/service/sageService';
import { Facture, FactureRequest, FactureUpdateRequest, RelanceRequest, RelanceResponse } from '@/types/factureType';
import { StatusRequest, StatusResponse } from '@/types/sageTypes';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* get all facture
*/
export const getFactures = createAsyncThunk(
'facture/getFactures',
async (): Promise<Facture[]> => {
try {
return await sageService.getFactures();
} catch (err) {
throw err;
}
}
);
/**
* get facture by id
*/
export const getFacture = createAsyncThunk(
'facture/getfacture',
async (id: string) => {
try {
const rep = await sageService.getFacture(id) as any
return rep.data as Facture
} catch (err) {
throw err;
}
}
);
/**
* valider facture
*/
export const validerFacture = createAsyncThunk(
'facture/validerFacture',
async (id: string) => {
try {
return await sageService.validerFacture(id)
} catch (err) {
throw err;
}
}
);
/**
* changed statut facture
*/
export const changerStatutFacture = createAsyncThunk(
'facture/changerStatutFacture',
async (data: StatusRequest): Promise<StatusResponse> => {
try {
return await sageService.changerStatut(60, data);
} catch (err) {
throw err;
}
}
);
/**
* relance facture
*/
export const relancerFacture = createAsyncThunk(
'facture/relancerFacture',
async (data: RelanceRequest): Promise<RelanceResponse> => {
try {
return await sageService.relancerFacture(data);
} catch (err) {
throw err;
}
}
);
/**
* create Facture
*/
export const createFacture = createAsyncThunk(
'facture/createFacture',
async (data: FactureRequest) => {
try {
return await sageService.createFacture(data);
} catch (err) {
throw err;
}
}
);
/**
* update Facture
*/
export const updateFacture = createAsyncThunk(
'facture/updateFacture',
async (
{ numero, data }: { numero: string; data: FactureUpdateRequest }
) => {
try {
return await sageService.updateFacture(numero, data);
} catch (err) {
throw err;
}
}
);
/**
* verified Factures
*/
export const selectFactureAsync = createAsyncThunk<
Facture,
Facture,
{ state: any }
>(
"facture/selectFactureAsync",
async (items, { dispatch }) => {
if (!items.lignes) {
const updateItem = await dispatch(getFacture(items.numero)).unwrap() as any
return updateItem;
}
return items;
}
);

View file

@ -0,0 +1,10 @@
import { Facture } from "@/types/factureType";
export interface factureState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
factures: Facture[];
factureSelected: Facture | null;
}

View file

@ -0,0 +1,10 @@
import type { RootState } from "@/store/store";
export const getAllfamilles = (state: RootState) => state.famille.familles;
export const getfamilleSelected = (state: RootState) => state.famille.familleSelected;
export const familleStatus = (state: RootState) => state.famille.status;
export const familleError = (state: RootState) => state.famille.error;

View file

@ -0,0 +1,74 @@
/* eslint-disable import/order */
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { Famille } from "@/types/familleType";
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { createFamille, getfamilles } from "./thunk";
import { familleState } from "./type";
const initialState: familleState = {
status: "idle",
error: null,
familles: [],
familleSelected: null,
};
const familleSlice = createSlice({
name: "famille",
initialState,
reducers: {
selectfamille: (state, action: PayloadAction<Famille>) => {
state.familleSelected = action.payload;
}
},
extraReducers: (builder) => {
/**
* get all familles
*/
builder.addCase(getfamilles.fulfilled, (state, action) => {
const data = action.payload;
state.familles = data;
state.error = null;
state.status = "succeeded";
});
builder.addCase(getfamilles.pending, (state) => {
state.status = "loading";
});
builder.addCase(getfamilles.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* add famille
*/
builder.addCase(createFamille.fulfilled, (state, action) => {
const data = action.payload as any
state.familles.unshift(data);
state.status = "succeeded";
state.error = null;
});
builder.addCase(createFamille.pending, (state) => {
state.status = "loading";
});
builder.addCase(createFamille.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
},
});
export const { selectfamille } = familleSlice.actions
export default familleSlice.reducer;

View file

@ -0,0 +1,39 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable import/order */
import { sageService } from '@/service/sageService';
import { Famille, FamilleRequest } from '@/types/familleType';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* get all familles
*/
export const getfamilles = createAsyncThunk(
'famille/getFamilles',
async (): Promise<Famille[]> => {
try {
return await sageService.getFamilles();
} catch (err) {
throw err;
}
}
);
/**
* add famille
*/
export const createFamille = createAsyncThunk(
'famille/createFamille',
async (data: FamilleRequest): Promise<Famille> => {
try {
return await sageService.createFamille(data);
} catch (err) {
throw err;
}
}
);

View file

@ -0,0 +1,10 @@
import { Famille } from "@/types/familleType";
export interface familleState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
familles: Famille[];
familleSelected: Famille | null;
}

View file

@ -0,0 +1,9 @@
import { RootState } from "@/store/store";
export const getAllfournisseurs = (state: RootState) => state.fournisseur.fournisseurs;
export const getfournisseurSelected = (state: RootState) => state.fournisseur.fournisseurSelected;
export const fournisseurStatus = (state: RootState) => state.fournisseur.status;
export const fournisseurError = (state: RootState) => state.fournisseur.error;

View file

@ -0,0 +1,185 @@
/* eslint-disable import/order */
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { fournisseurState } from "./type";
import { Fournisseur } from "@/types/fournisseurType";
import { addFournisseur, getFournisseurs, updateFournisseur } from "./thunk";
import { addContactFournisseur, updateContactFournisseur } from "../contact/thunk";
import { Contacts } from "@/types/clientType";
const initialState: fournisseurState = {
status: "idle",
error: null,
fournisseurs: [],
fournisseurSelected: null,
};
const fournisseurSlice = createSlice({
name: "fournisseur",
initialState,
reducers: {
selectfournisseur: (state, action: PayloadAction<Fournisseur>) => {
state.fournisseurSelected = action.payload;
}
},
extraReducers: (builder) => {
/**
* get all fournisseurs
*/
builder.addCase(getFournisseurs.fulfilled, (state, action) => {
const data = action.payload;
state.fournisseurs = data;
state.error = null;
state.status = "succeeded";
});
builder.addCase(getFournisseurs.pending, (state) => {
state.status = "loading";
});
builder.addCase(getFournisseurs.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* add fournisseur
*/
builder.addCase(addFournisseur.fulfilled, (state, action) => {
const { data, success } = action.payload as any
if(success){
state.fournisseurs.push(data);
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(addFournisseur.pending, (state) => {
state.status = "loading";
});
builder.addCase(addFournisseur.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* update fournisseur
*/
builder.addCase(updateFournisseur.pending, (state) => {
state.status = "loading";
});
builder.addCase(updateFournisseur.fulfilled, (state, action) => {
console.log("action.payload : ",action.payload);
const { fournisseur, success } = action.payload as unknown as { success: boolean; fournisseur: Fournisseur };
if (success && fournisseur?.numero) {
const idx = state.fournisseurs.findIndex((p) => p.numero === fournisseur.numero);
if (idx !== -1) {
const oldItem = state.fournisseurs[idx];
state.fournisseurs[idx] = {
...oldItem,
intitule: fournisseur.intitule,
adresse: fournisseur.adresse,
code_postal: fournisseur.code_postal,
ville: fournisseur.ville,
telephone: fournisseur.telephone,
email: fournisseur.email,
siret: fournisseur.siret,
tva_intra: fournisseur.tva_intra,
code_naf: fournisseur.code_naf,
};
}
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(updateFournisseur.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "Failed to update";
});
/**
* create contact Fournisseur
*/
builder.addCase(addContactFournisseur.pending, (state) => {
state.status = "loading";
});
builder.addCase(addContactFournisseur.fulfilled, (state, action) => {
const payload = action.payload as Contacts;
const idx = state.fournisseurs.findIndex((n) => n.numero === payload.numero);
if (idx !== -1) {
state.fournisseurs[idx].contacts?.unshift(payload);
}
if (state.fournisseurSelected?.numero === payload.numero) {
state.fournisseurSelected.contacts?.unshift(payload)
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(addContactFournisseur.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* update contact Fournisseur
*/
builder.addCase(updateContactFournisseur.pending, (state) => {
state.status = "loading";
});
builder.addCase(updateContactFournisseur.fulfilled, (state, action) => {
const payload = action.payload as Contacts;
const idx = state.fournisseurs.findIndex((n) => n.numero === payload.numero);
if (idx !== -1) {
const idxContact = state.fournisseurs[idx].contacts?.findIndex((n) => n.contact_numero === payload.contact_numero);
if (idxContact !== -1) {
state.fournisseurs[idx].contacts![idxContact || 0] = payload
}
}
if (state.fournisseurSelected?.numero === payload.numero) {
const idxContact = state.fournisseurSelected.contacts?.findIndex((n) => n.contact_numero === payload.contact_numero);
if (idxContact !== -1) {
state.fournisseurSelected.contacts![idxContact || 0] = payload
}
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(updateContactFournisseur.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
},
});
export const { selectfournisseur } = fournisseurSlice.actions
export default fournisseurSlice.reducer;

View file

@ -0,0 +1,58 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable import/order */
import { sageService } from '@/service/sageService';
import { Fournisseur, FournisseurRequest, FournisseurUpdateRequest } from '@/types/fournisseurType';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* get all fournisseurs
*/
export const getFournisseurs = createAsyncThunk(
'fournisseur/getFournisseurs',
async (): Promise<Fournisseur[]> => {
try {
return await sageService.getFournisseurs();
} catch (err) {
throw err;
}
}
);
/**
* add fournisseur
*/
export const addFournisseur = createAsyncThunk(
'fournisseur/addFournisseur',
async (data: FournisseurRequest): Promise<Fournisseur> => {
try {
return await sageService.addFournisseur(data);
} catch (err) {
throw err;
}
}
);
/**
* update fournisseur
*/
export const updateFournisseur = createAsyncThunk(
'fournisseur/updateFournisseur',
async (
{ numero, data }: { numero: string; data: FournisseurUpdateRequest }
): Promise<Fournisseur> => {
try {
return await sageService.updateFournisseur(numero, data);
} catch (err) {
throw err;
}
}
);

View file

@ -0,0 +1,11 @@
import { Fournisseur } from "@/types/fournisseurType";
export interface fournisseurState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
fournisseurs: Fournisseur[];
fournisseurSelected: Fournisseur | null;
}

View file

@ -0,0 +1,10 @@
import type { RootState } from "@/store/store";
export const getSociete = (state: RootState) => state.gateways.societe;
export const getAllGateways = (state: RootState) => state.gateways.gateways;
export const getgatewaysSelected = (state: RootState) => state.gateways.gatewaysSelected;
export const gatewaysStatus = (state: RootState) => state.gateways.status;
export const gatewaysError = (state: RootState) => state.gateways.error;

View file

@ -0,0 +1,123 @@
/* eslint-disable import/order */
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { gatewaysState } from "./type";
import { Gateways, GatewaysResponse } from "@/types/gateways";
import { addGateways, getGateways, getGatewaysById, getSociete } from "./thunk";
const initialState: gatewaysState = {
status: "idle",
error: null,
gateways: [],
gatewaysSelected: null,
societe: null
};
const gatewaysSlice = createSlice({
name: "gateways",
initialState,
reducers: {
selectgateways: (state, action: PayloadAction<Gateways>) => {
state.gatewaysSelected = action.payload;
}
},
extraReducers: (builder) => {
/**
* get societe
*/
builder.addCase(getSociete.fulfilled, (state, action) => {
const data = action.payload
console.log("data : ",data);
state.societe = data;
state.error = null;
state.status = "succeeded";
});
builder.addCase(getSociete.pending, (state) => {
state.status = "loading";
});
builder.addCase(getSociete.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* get all gateways
*/
builder.addCase(getGateways.fulfilled, (state, action) => {
const data = action.payload
state.gateways = data.items;
state.error = null;
state.status = "succeeded";
});
builder.addCase(getGateways.pending, (state) => {
state.status = "loading";
});
builder.addCase(getGateways.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* get devis by id
*/
builder.addCase(getGatewaysById.pending, (state) => {
state.status = "loading";
});
builder.addCase(getGatewaysById.fulfilled, (state, action) => {
const data = action.payload as Gateways;
const idx = state.gateways.findIndex((n) => n.id === data.id);
if (idx !== -1) {
state.gateways[idx] = data;
} else {
state.gateways.push(data);
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(getGatewaysById.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* add gateways
*/
builder.addCase(addGateways.fulfilled, (state, action) => {
const data = action.payload as any
state.gateways.unshift(data);
state.status = "succeeded";
state.error = null;
});
builder.addCase(addGateways.pending, (state) => {
state.status = "loading";
});
builder.addCase(addGateways.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
},
});
export const { selectgateways } = gatewaysSlice.actions
export default gatewaysSlice.reducer;

View file

@ -0,0 +1,71 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable import/order */
import { sageService } from '@/service/sageService';
import { Gateways, GatewaysRequest, GatewaysResponse } from '@/types/gateways';
import { Societe } from '@/types/societeType';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* get societe
*/
export const getSociete = createAsyncThunk(
'gateways/getSociete',
async (): Promise<Societe> => {
try {
return await sageService.getSociete();
} catch (err) {
throw err;
}
}
);
/**
* get all gateways
*/
export const getGateways = createAsyncThunk(
'gateways/getGateways',
async (): Promise<GatewaysResponse> => {
try {
return await sageService.getGateways();
} catch (err) {
throw err;
}
}
);
/**
* get gateways by id
*/
export const getGatewaysById = createAsyncThunk(
'gateways/gatewaysById',
async (id: string): Promise<Gateways> => {
try {
return await sageService.getGatewaysById(id);
} catch (err) {
throw err;
}
}
);
/**
* add gateways
*/
export const addGateways = createAsyncThunk(
'gateways/addGateways',
async (data: GatewaysRequest): Promise<Gateways> => {
try {
return await sageService.addGateways(data);
} catch (err) {
throw err;
}
}
);

View file

@ -0,0 +1,12 @@
import { Gateways } from "@/types/gateways";
import { Societe } from "@/types/societeType";
export interface gatewaysState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
gateways: Gateways[];
gatewaysSelected: Gateways | null;
societe: Societe | null;
}

View file

@ -0,0 +1,60 @@
import type { RootState } from "@/store/store";
/**
* REGLEMENT
*/
export const getAllReglements = (state: RootState) => state.reglement.reglements;
export const getreglementSelected = (state: RootState) => state.reglement.reglementSelected;
/**
* STATISTIQUE
*/
export const getReglementStats = (state: RootState) => state.reglement.statistiques;
/**
* MODES
*/
export const getAllModes = (state: RootState) => state.reglement.modes;
export const getModeSelected = (state: RootState) => state.reglement.modeSelected;
/**
* DEVISES
*/
export const getAllDevises = (state: RootState) => state.reglement.devises;
export const getDeviseSelected = (state: RootState) => state.reglement.deviseSelected;
/**
* TRÉSORERIES
*/
export const getAllTresoreries = (state: RootState) => state.reglement.tresoreries;
export const getTresorerieSelected = (state: RootState) =>
state.reglement.tresorerieSelected;
/**
* COMPTES GÉNÉRAUX
*/
export const getAllCompteGeneraux = (state: RootState) =>
state.reglement.CompteGeneraux;
export const getCompteGenerauxSelected = (state: RootState) =>
state.reglement.CompteGenerauxSelected;
/**
* TAUX
*/
export const getAllTaux = (state: RootState) => state.reglement.taux;
export const getTauxSelected = (state: RootState) => state.reglement.tauxSelected;
/**
* ENCAISSEMENTS
*/
export const getAllEncaissements = (state: RootState) => state.reglement.encaissements;
/**
* GLOBAL
*/
export const reglementStatus = (state: RootState) => state.reglement.status;
export const reglementError = (state: RootState) => state.reglement.error;

View file

@ -0,0 +1,226 @@
/* eslint-disable import/order */
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { createSlice, isFulfilled, isPending, isRejected, type PayloadAction } from "@reduxjs/toolkit";
import { reglementState } from "./type";
import { Comptes, ComptesResponse, Devise, DeviseResponse, Encaissement, EncaissementResponse, FacturesReglement, Mode, ModeResponse, ReglementsIdResponse, reglementsResponse, Statistique, Taux, TauxResponse, Tresorerie, TresorerieResponse } from "@/types/reglementType";
import { getCompteGeneraux, getDevises, getMode, getTaux, getTresorerie, getEncaissement, loadAllReglementData, getReglement, reglerFacture, getReglementId } from "./thunk";
const initialState: reglementState = {
status: "idle",
error: null,
reglements: [],
reglementSelected: null,
statistiques: null,
modes: [],
modeSelected: null,
devises: [],
deviseSelected: null,
tresoreries: [],
tresorerieSelected: null,
CompteGeneraux: [],
CompteGenerauxSelected: null,
taux: [],
tauxSelected: null,
encaissements: null,
};
const reglementSlice = createSlice({
name: "reglement",
initialState,
reducers: {
selectReglement: (state, action: PayloadAction<FacturesReglement>) => {
state.reglementSelected = action.payload;
},
selectMode: (state, action: PayloadAction<Mode>) => {
state.modeSelected = action.payload;
},
selectDevise: (state, action: PayloadAction<Devise>) => {
state.deviseSelected = action.payload;
},
selectTresorerie: (state, action: PayloadAction<Tresorerie>) => {
state.tresorerieSelected = action.payload;
},
selectCompteGeneraux: (state, action: PayloadAction<Comptes>) => {
state.CompteGenerauxSelected = action.payload;
},
selectTaux: (state, action: PayloadAction<Taux>) => {
state.tauxSelected = action.payload;
},
selectEncaissement: (state, action: PayloadAction<Encaissement>) => {
state.encaissements = action.payload;
},
},
extraReducers: (builder) => {
/**
* regler facture
*/
builder.addCase(reglerFacture.pending, (state) => {
state.status = "loading";
});
builder.addCase(reglerFacture.fulfilled, (state) => {
state.status = "succeeded";
state.error = null;
});
builder.addCase(reglerFacture.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* get reglement by id
*/
builder.addCase(getReglementId.pending, (state) => {
state.status = "loading";
});
builder.addCase(getReglementId.fulfilled, (state, action) => {
const { success, data } = action.payload as ReglementsIdResponse;
if(success) {
const idx = state.reglements.findIndex((n) => n.numero === data.numero);
if (idx !== -1) {
state.reglements[idx] = data;
} else {
state.reglements.unshift(data);
}
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(getReglementId.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
/**
* LOAD ALL DATA - Handler unifié pour charger toutes les données
*/
builder.addCase(loadAllReglementData.fulfilled, (state, action) => {
const { reglements, modes, devises, tresoreries, comptes, taux } = action.payload;
if (reglements){
state.reglements = reglements.factures;
state.statistiques = reglements.statistiques;
}
if (modes) state.modes = modes;
if (devises) state.devises = devises;
if (tresoreries) state.tresoreries = tresoreries;
if (comptes) state.CompteGeneraux = comptes;
if (taux) state.taux = taux;
});
/**
* SUCCESS HANDLERS - Handlers individuels (optionnels, pour les appels individuels)
*/
builder
.addCase(getReglement.fulfilled, (state, action) => {
const { data, success } = action.payload as reglementsResponse;
if (success) {
state.reglements = data.factures;
state.statistiques = data.statistiques;
}
})
.addCase(getMode.fulfilled, (state, action) => {
const { data, success } = action.payload as ModeResponse;
if (success) state.modes = data.modes;
})
.addCase(getDevises.fulfilled, (state, action) => {
const { data, success } = action.payload as DeviseResponse;
if (success) state.devises = data.devises;
})
.addCase(getTresorerie.fulfilled, (state, action) => {
const { data, success } = action.payload as TresorerieResponse;
if (success) state.tresoreries = data.journaux;
})
.addCase(getCompteGeneraux.fulfilled, (state, action) => {
const { data, success } = action.payload as ComptesResponse;
if (success) state.CompteGeneraux = data.comptes;
})
.addCase(getTaux.fulfilled, (state, action) => {
const { data, success } = action.payload as TauxResponse;
if (success) state.taux = data.taux;
})
.addCase(getEncaissement.fulfilled, (state, action) => {
const { data, success } = action.payload as EncaissementResponse;
if (success) state.encaissements = data;
});
/**
* GLOBAL MATCHERS
*/
builder
.addMatcher(
isPending(
loadAllReglementData,
getReglement,
getMode,
getDevises,
getTresorerie,
getCompteGeneraux,
getTaux,
getEncaissement
),
(state) => {
state.status = "loading";
state.error = null;
}
)
.addMatcher(
isFulfilled(
loadAllReglementData,
getReglement,
getMode,
getDevises,
getTresorerie,
getCompteGeneraux,
getTaux,
getEncaissement
),
(state) => {
state.status = "succeeded";
state.error = null;
}
)
.addMatcher(
isRejected(
loadAllReglementData,
getReglement,
getMode,
getDevises,
getTresorerie,
getCompteGeneraux,
getTaux,
getEncaissement
),
(state, action) => {
state.status = "failed";
state.error = action.error.message ?? "An error occurred";
}
);
},
});
export const { selectReglement, selectMode, selectDevise, selectTresorerie, selectCompteGeneraux, selectTaux, selectEncaissement } = reglementSlice.actions;
export default reglementSlice.reducer;

View file

@ -0,0 +1,177 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable import/order */
import { sageService } from '@/service/sageService';
import { ComptesResponse, DeviseResponse, EncaissementResponse, ModeResponse, ReglementPostResponse, ReglementRequest, ReglementsIdResponse, reglementsResponse, TauxResponse, TresorerieResponse } from '@/types/reglementType';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* Thunk unifié pour charger toutes les données de règlement en parallèle
*/
export const loadAllReglementData = createAsyncThunk(
'reglement/loadAll',
async (_, { rejectWithValue }) => {
try {
// Lancer tous les appels en parallèle avec Promise.all
const [reglementsRes, modesRes, devisesRes, tresoreriesRes, comptesRes, tauxRes] = await Promise.all([
sageService.getReglement().catch(() => ({ success: false, data: {} })),
sageService.getMode().catch(() => ({ success: false, data: { modes: [] } })),
sageService.getDevises().catch(() => ({ success: false, data: { devises: [] } })),
sageService.getTresorerie().catch(() => ({ success: false, data: { journaux: [] } })),
sageService.getCompteGeneraux().catch(() => ({ success: false, data: { comptes: [] } })),
sageService.getTaux().catch(() => ({ success: false, data: { taux: [] } })),
]);
// Extraire les données de chaque réponse
return {
reglements: (reglementsRes as reglementsResponse)?.data || [],
modes: (modesRes as ModeResponse)?.data?.modes || [],
devises: (devisesRes as DeviseResponse)?.data?.devises || [],
tresoreries: (tresoreriesRes as TresorerieResponse)?.data?.journaux || [],
comptes: (comptesRes as ComptesResponse)?.data?.comptes || [],
taux: (tauxRes as TauxResponse)?.data?.taux || [],
};
} catch (error: any) {
return rejectWithValue(error.message || "Erreur lors du chargement des données de règlement");
}
}
);
/**
* regler facture
*/
export const reglerFacture = createAsyncThunk(
'reglement/reglerFacture',
async (data: ReglementRequest): Promise<ReglementPostResponse> => {
try {
return await sageService.reglerFacture(data);
} catch (err) {
throw err;
}
}
);
/**
* get reglement by id
*/
export const getReglementId = createAsyncThunk(
'reglement/getReglementId',
async (id: string): Promise<ReglementsIdResponse> => {
try {
return await sageService.getReglementId(id);
} catch (err) {
throw err;
}
}
);
/**
* get reglement
*/
export const getReglement = createAsyncThunk(
'reglement/getReglement',
async (): Promise<reglementsResponse> => {
try {
return await sageService.getReglement();
} catch (err) {
throw err;
}
}
);
/**
* get mode
*/
export const getMode = createAsyncThunk(
'reglement/getMode',
async (): Promise<ModeResponse> => {
try {
return await sageService.getMode();
} catch (err) {
throw err;
}
}
);
/**
* get devises
*/
export const getDevises = createAsyncThunk(
'reglement/getDevises',
async (): Promise<DeviseResponse> => {
try {
return await sageService.getDevises();
} catch (err) {
throw err;
}
}
);
/**
* get trésorerie
*/
export const getTresorerie = createAsyncThunk(
'reglement/getTresorerie',
async (): Promise<TresorerieResponse> => {
try {
return await sageService.getTresorerie();
} catch (err) {
throw err;
}
}
);
/**
* get comptes généraux
*/
export const getCompteGeneraux = createAsyncThunk(
'reglement/getCompteGeneraux',
async (): Promise<ComptesResponse> => {
try {
return await sageService.getCompteGeneraux();
} catch (err) {
throw err;
}
}
);
/**
* get taux
*/
export const getTaux = createAsyncThunk(
'reglement/getTaux',
async (): Promise<TauxResponse> => {
try {
return await sageService.getTaux();
} catch (err) {
throw err;
}
}
);
/**
* get encaissement
*/
export const getEncaissement = createAsyncThunk(
'reglement/getEncaissement',
async (): Promise<EncaissementResponse> => {
try {
return await sageService.getEncaissement();
} catch (err) {
throw err;
}
}
);

View file

@ -0,0 +1,29 @@
import { Comptes, Devise, Encaissement, FacturesReglement, Mode, Statistique, Taux, Tresorerie } from "@/types/reglementType";
export interface reglementState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
reglements: FacturesReglement[]
reglementSelected: FacturesReglement | null;
statistiques: Statistique | null;
modes: Mode[];
modeSelected: Mode | null;
encaissements: Encaissement | null;
devises: Devise[];
deviseSelected: Devise | null;
tresoreries: Tresorerie[];
tresorerieSelected: Tresorerie | null;
CompteGeneraux: Comptes[];
CompteGenerauxSelected: Comptes | null;
taux: Taux[];
tauxSelected: Taux | null;
}

View file

@ -0,0 +1,6 @@
// store/sageBuilder/selector.ts
import type { RootState } from "@/store/store";
export const getSageBuilderHtmlContent = (state: RootState) => state.sageBuilder.htmlContent;
export const sageBuilderStatus = (state: RootState) => state.sageBuilder.status;
export const sageBuilderError = (state: RootState) => state.sageBuilder.error;

View file

@ -0,0 +1,45 @@
// store/sageBuilder/slice.ts
import { createSlice } from "@reduxjs/toolkit";
import { SageBuilderState } from "./type";
import { getSageBuilderDashboard } from "./thunk";
const initialState: SageBuilderState = {
status: "idle",
error: null,
htmlContent: null,
};
const sageBuilderSlice = createSlice({
name: "sageBuilder",
initialState,
reducers: {
resetSageBuilder: (state) => {
state.status = "idle";
state.error = null;
state.htmlContent = null;
}
},
extraReducers: (builder) => {
/**
* Get Sage Builder Dashboard
*/
builder.addCase(getSageBuilderDashboard.fulfilled, (state, action) => {
state.htmlContent = action.payload;
state.error = null;
state.status = "succeeded";
});
builder.addCase(getSageBuilderDashboard.pending, (state) => {
state.status = "loading";
state.error = null;
});
builder.addCase(getSageBuilderDashboard.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "Une erreur est survenue";
});
},
});
export const { resetSageBuilder } = sageBuilderSlice.actions;
export default sageBuilderSlice.reducer;

View file

@ -0,0 +1,28 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* Fetch Sage Builder Dashboard HTML
*/
export const getSageBuilderDashboard = createAsyncThunk(
'sageBuilder/getDashboard',
async (): Promise<string> => {
try {
const response = await fetch(
'https://api.sage-ai-studio.webexpr.dev/api/embed/artifact/968ba24a-47d4-4ac4-b838-f1f7a60c310d?mode=light&theme=dataven',
{
headers: {
'Authorization': 'Bearer sk_live_bef70d4dd6bc671fd815d21104d5dce8'
}
}
);
if (!response.ok) {
throw new Error('Erreur de chargement du dashboard');
}
return await response.text();
} catch (err) {
throw err;
}
}
);

View file

@ -0,0 +1,5 @@
export interface SageBuilderState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
htmlContent: string | null;
}

View file

@ -0,0 +1,10 @@
import type { RootState } from "@/store/store";
export const getAlluniversign = (state: RootState) => state.universign.universign;
export const getuniversignSelected = (state: RootState) => state.universign.universignSelected;
export const universignStatus = (state: RootState) => state.universign.status;
export const universignError = (state: RootState) => state.universign.error;

View file

@ -0,0 +1,84 @@
/* eslint-disauniversigne import/order */
/* eslint-disauniversigne @typescript-eslint/consistent-type-imports */
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { universignState } from "./type";
import { UniversignType } from "@/types/sageTypes";
import { getUniversigns, universignId } from "./thunk";
const initialState: universignState = {
status: "idle",
error: null,
universign: [],
universignSelected: null,
};
const universignSlice = createSlice({
name: "universign",
initialState,
reducers: {
selectuniversign: (state, action: PayloadAction<UniversignType>) => {
state.universignSelected = action.payload;
}
},
extraReducers: (builder) => {
/**
* get all universign
*/
builder.addCase(getUniversigns.fulfilled, (state, action) => {
const data = action.payload;
state.universign = data;
state.error = null;
state.status = "succeeded";
});
builder.addCase(getUniversigns.pending, (state) => {
state.status = "loading";
});
builder.addCase(getUniversigns.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
/**
* get universign by id
*/
builder.addCase(universignId.pending, (state) => {
state.status = "loading";
});
builder.addCase(universignId.fulfilled, (state, action) => {
const payload = action.payload as any;
const data = payload.data as UniversignType
const idx = state.universign.findIndex((n) => n.transaction_id === data.transaction_id);
if (idx !== -1) {
state.universign[idx] = data;
} else {
state.universign.push(data);
}
state.status = "succeeded";
state.error = null;
});
builder.addCase(universignId.rejected, (state, action) => {
state.status = "failed";
state.error = (action.payload as string) || action.error.message || "An error occurred";
});
},
});
export const { selectuniversign } = universignSlice.actions
export default universignSlice.reducer;

View file

@ -0,0 +1,52 @@
/* eslint-disauniversigne import/no-unresolved */
/* eslint-disauniversigne import/order */
import { sageService } from '@/service/sageService';
import { UniversignType } from '@/types/sageTypes';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* get all universign
*/
export const getUniversigns = createAsyncThunk(
'universign/getUniversigns',
async (): Promise<UniversignType[]> => {
try {
return await sageService.universign();
} catch (err) {
throw err;
}
}
);
/**
* get universign by id
*/
export const universignId = createAsyncThunk(
'universign/universignId',
async (id: string): Promise<UniversignType> => {
try {
return await sageService.universignId(id);
} catch (err) {
throw err;
}
}
);
/**
* download pdf
*/
export const downloadSignedDocument = createAsyncThunk(
'universign/downloadSignedDocument',
async (transactionId: string): Promise<Blob> => {
try {
return await sageService.downloadSignedDocument(transactionId);
} catch (err) {
throw err;
}
}
);

View file

@ -0,0 +1,9 @@
import { UniversignType } from "@/types/sageTypes";
export interface universignState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
universign: UniversignType[];
universignSelected: UniversignType | null;
}

View file

@ -0,0 +1,10 @@
import type { RootState } from "@/store/store";
export const getAllusers = (state: RootState) => state.user.users;
export const getuserSelected = (state: RootState) => state.user.userSelected;
export const getuserConnected = (state: RootState) => state.user.userConnected;
export const userStatus = (state: RootState) => state.user.status;
export const userError = (state: RootState) => state.user.error;

View file

@ -0,0 +1,54 @@
/* eslint-disable import/order */
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { UserInterface } from "@/types/userInterface";
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { userState } from "./type";
import { authMe } from "./thunk";
const initialState: userState = {
status: "idle",
error: null,
users: [],
userSelected: null,
userConnected: null
};
const userSlice = createSlice({
name: "user",
initialState,
reducers: {
selectuser: (state, action: PayloadAction<UserInterface>) => {
state.userSelected = action.payload;
}
},
extraReducers: (builder) => {
/**
* auth me
*/
builder.addCase(authMe.fulfilled, (state, action) => {
const data = action.payload;
state.userConnected = data;
state.error = null;
state.status = "succeeded";
});
builder.addCase(authMe.pending, (state) => {
state.status = "loading";
});
builder.addCase(authMe.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "An error occurred";
});
},
});
export const { selectuser } = userSlice.actions
export default userSlice.reducer;

View file

@ -0,0 +1,23 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable import/order */
import { sageService } from '@/service/sageService';
import { UserInterface } from '@/types/userInterface';
import { createAsyncThunk } from '@reduxjs/toolkit';
/**
* user auth
*/
export const authMe = createAsyncThunk(
'user/authMe',
async (): Promise<UserInterface> => {
try {
return await sageService.authMe();
} catch (err) {
throw err;
}
}
);

View file

@ -0,0 +1,10 @@
import { UserInterface } from "@/types/userInterface";
export interface userState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
users: UserInterface[];
userSelected: UserInterface | null;
userConnected: UserInterface | null;
}

18
src/store/hooks.ts Normal file
View file

@ -0,0 +1,18 @@
// import { useDispatch, useSelector, useStore } from 'react-redux';
// import type { TypedUseSelectorHook } from 'react-redux';
// import type { RootState, AppDispatch, AppStore } from './store';
// export const useAppDispatch: () => AppDispatch = useDispatch;
// export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// export const useAppStore: () => AppStore = useStore;
import { useDispatch, useSelector, type TypedUseSelectorHook } from 'react-redux';
import { RootState, store } from "./store";
export const useAppDispatch: () => typeof store.dispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View file

@ -0,0 +1,29 @@
/* eslint-disable import/no-unresolved */
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getArticles } from "../features/article/thunk";
export function useArticles() {
const dispatch = useDispatch<any>();
const status = useSelector((state: any) => state.article.status);
const [loading, setLoading] = useState(false);
useEffect(() => {
const load = async () => {
if (status === "idle") {
setLoading(true);
await dispatch(getArticles()).unwrap();
setLoading(false);
}
};
load();
}, [dispatch, status]);
return { loading, status };
}

29
src/store/hooks/client.ts Normal file
View file

@ -0,0 +1,29 @@
/* eslint-disable import/no-unresolved */
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getClients } from "../features/client/thunk";
export function useClients() {
const dispatch = useDispatch<any>();
const clientsStatus = useSelector((state: any) => state.client.status);
const [loading, setLoading] = useState(false);
useEffect(() => {
const load = async () => {
if (clientsStatus === "idle") {
setLoading(true);
await dispatch(getClients()).unwrap();
setLoading(false);
}
};
load();
}, [dispatch, clientsStatus]);
return { loading, clientsStatus };
}

View file

@ -0,0 +1,29 @@
/* eslint-disable import/no-unresolved */
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getCommandes } from "../features/commande/thunk";
export function useCommande() {
const dispatch = useDispatch<any>();
const status = useSelector((state: any) => state.commande.status);
const [loading, setLoading] = useState(false);
useEffect(() => {
const load = async () => {
if (status === "idle") {
setLoading(true);
await dispatch(getCommandes()).unwrap();
setLoading(false);
}
};
load();
}, [dispatch, status]);
return { loading, status };
}

29
src/store/hooks/devis.ts Normal file
View file

@ -0,0 +1,29 @@
/* eslint-disable import/no-unresolved */
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getDevisList } from "../features/devis/thunk";
export function useDevis() {
const dispatch = useDispatch<any>();
const status = useSelector((state: any) => state.devis.status);
const [loading, setLoading] = useState(false);
useEffect(() => {
const load = async () => {
if (status === "idle") {
setLoading(true);
await dispatch(getDevisList()).unwrap();
setLoading(false);
}
};
load();
}, [dispatch, status]);
return { loading, status };
}

View file

@ -0,0 +1,252 @@
// hooks/useAppData.ts
import { useCallback, useEffect, useState } from 'react';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
// Thunks
import { getClients } from '@/store/features/client/thunk';
import { getDevisList } from '@/store/features/devis/thunk';
import { getArticles } from '@/store/features/article/thunk';
import { getFactures } from '@/store/features/factures/thunk';
import { getCommercials } from '@/store/features/commercial/thunk';
import { getCommandes } from '@/store/features/commande/thunk';
import { getAvoirs } from '@/store/features/avoir/thunk';
import { getfamilles } from '@/store/features/famille/thunk';
import { getUniversigns } from '@/store/features/universign/thunk';
// Selectors pour les status
import { clientStatus } from '@/store/features/client/selectors';
import { devisStatus } from '@/store/features/devis/selectors';
import { articleStatus } from '@/store/features/article/selectors';
import { factureStatus } from '@/store/features/factures/selectors';
import { commercialsStatus } from '@/store/features/commercial/selectors';
import { commandeStatus } from '@/store/features/commande/selectors';
import { avoirStatus } from '@/store/features/avoir/selectors';
import { familleStatus } from '@/store/features/famille/selectors';
import { universignStatus } from '@/store/features/universign/selectors';
import { loadAllReglementData } from '../features/reglement/thunk';
import { reglementStatus } from '../features/reglement/selectors';
// Types pour les données à charger
export type DataType =
| 'clients'
| 'devis'
| 'articles'
| 'factures'
| 'reglement'
| 'commercials'
| 'commandes'
| 'avoirs'
| 'familles'
| 'universign'
// Configuration des thunks par type de données
const dataConfig: Record<DataType, { thunk: any; statusSelector: any }> = {
clients: { thunk: getClients, statusSelector: clientStatus },
devis: { thunk: getDevisList, statusSelector: devisStatus },
articles: { thunk: getArticles, statusSelector: articleStatus },
factures: { thunk: getFactures, statusSelector: factureStatus },
reglement: { thunk: loadAllReglementData, statusSelector: reglementStatus },
commercials: { thunk: getCommercials, statusSelector: commercialsStatus },
commandes: { thunk: getCommandes, statusSelector: commandeStatus },
avoirs: { thunk: getAvoirs, statusSelector: avoirStatus },
familles: { thunk: getfamilles, statusSelector: familleStatus },
universign: { thunk: getUniversigns, statusSelector: universignStatus },
};
// Données par défaut à charger sur toutes les pages
const DEFAULT_DATA: DataType[] = [
'clients',
'articles',
'commercials',
'familles',
];
interface UseAppDataOptions {
include?: DataType[];
only?: DataType[];
exclude?: DataType[];
forceRefresh?: boolean;
skipInitialLoad?: boolean;
}
interface UseAppDataReturn {
refresh: () => Promise<void>;
refreshData: (...types: DataType[]) => Promise<void>;
isLoading: boolean;
isInitialLoading: boolean;
error: string | null;
statuses: Record<DataType, string>;
}
export const useAppData = (options: UseAppDataOptions = {}): UseAppDataReturn => {
const dispatch = useAppDispatch();
const [isLoading, setIsLoading] = useState(false);
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Récupérer tous les status
const statuses: Record<DataType, string> = {
clients: useAppSelector(clientStatus),
devis: useAppSelector(devisStatus),
articles: useAppSelector(articleStatus),
factures: useAppSelector(factureStatus),
reglement: useAppSelector(reglementStatus),
commercials: useAppSelector(commercialsStatus),
commandes: useAppSelector(commandeStatus),
avoirs: useAppSelector(avoirStatus),
familles: useAppSelector(familleStatus),
universign: useAppSelector(universignStatus),
};
// Déterminer les données à charger
const getDataToLoad = useCallback((): DataType[] => {
const { include = [], only, exclude = [] } = options;
if (only) {
return only;
}
const baseData = [...DEFAULT_DATA, ...include];
return baseData.filter((type) => !exclude.includes(type));
}, [options]);
// Charger des données spécifiques
const loadData = useCallback(
async (types: DataType[], force = false) => {
const promises = types.map(async (type) => {
const config = dataConfig[type];
if (!config) return;
// Ne charger que si status est 'idle' ou si force est true
const currentStatus = statuses[type];
if (!force && currentStatus !== 'idle') return;
try {
await dispatch(config.thunk()).unwrap();
} catch (err) {
console.error(`Erreur chargement ${type}:`, err);
throw err;
}
});
await Promise.all(promises);
},
[dispatch, statuses]
);
// Rafraîchir toutes les données configurées
const refresh = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const dataToLoad = getDataToLoad();
await loadData(dataToLoad, true); // Force refresh
} catch (err: any) {
setError(err.message || 'Erreur lors du rafraîchissement');
} finally {
setIsLoading(false);
}
}, [getDataToLoad, loadData]);
// Rafraîchir des données spécifiques
const refreshData = useCallback(
async (...types: DataType[]) => {
setIsLoading(true);
setError(null);
try {
await loadData(types, true); // Force refresh
} catch (err: any) {
setError(err.message || 'Erreur lors du rafraîchissement');
} finally {
setIsLoading(false);
}
},
[loadData]
);
// Chargement initial au montage
useEffect(() => {
if (options.skipInitialLoad) {
setIsInitialLoading(false);
return;
}
const initialLoad = async () => {
setIsInitialLoading(true);
setError(null);
try {
const dataToLoad = getDataToLoad();
await loadData(dataToLoad, options.forceRefresh);
} catch (err: any) {
setError(err.message || 'Erreur lors du chargement initial');
} finally {
setIsInitialLoading(false);
}
};
initialLoad();
}, []); // Exécuter une seule fois au montage
return {
refresh,
refreshData,
isLoading,
isInitialLoading,
error,
statuses,
};
};
// ============================================
// HOOKS SPÉCIALISÉS PAR PAGE
// ============================================
/** Hook pour la page Devis */
export const useDevisData = () => {
return useAppData({
include: ['devis', 'universign'],
});
};
/** Hook pour la page Factures */
export const useFacturesData = () => {
return useAppData({
include: ['factures', 'avoirs'],
});
};
/** Hook pour la page Commandes */
export const useCommandesData = () => {
return useAppData({
include: ['commandes'],
});
};
/** Hook pour la page Articles */
export const useArticlesData = () => {
return useAppData({
include: ['articles', 'familles'],
});
};
/** Hook pour le Dashboard (toutes les données) */
export const useDashboardData = () => {
return useAppData({
include: ['devis', 'factures', 'commandes', 'avoirs', 'reglement'],
});
};
/** Hook pour la création de documents (devis, factures, etc.) */
export const useDocumentCreationData = () => {
return useAppData({
only: ['clients', 'articles', 'familles', 'commercials'],
});
};
export default useAppData;

2
src/store/resetAction.ts Normal file
View file

@ -0,0 +1,2 @@
export const RESET_APP = 'RESET_APP';
export const resetApp = () => ({ type: RESET_APP });

87
src/store/store.ts Normal file
View file

@ -0,0 +1,87 @@
import { configureStore } from '@reduxjs/toolkit';
import { combineReducers } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage'; // Utilisez localStorage par défaut
import clientReducer from "./features/client/slice";
import articleReducer from "./features/article/slice";
import devisReducer from "./features/devis/slice";
import commandeReducer from "./features/commande/slice";
import factureReducer from "./features/factures/slice";
import BLReducer from "./features/bl/slice";
import fournisseurReducer from "./features/fournisseur/slice";
import avoirReducer from "./features/avoir/slice";
import familleReducer from "./features/famille/slice";
import contactReducer from "./features/contact/slice";
import commercialsReducer from "./features/commercial/slice"
import gatewaysReducer from "./features/gateways/slice";
import universignReducer from "./features/universign/slice";
import userReducer from "./features/user/slice";
import entrepriseReducer from "./features/entreprise/slice"
import sageBuilderReducer from "./features/sage-builder/slice";
import reglementReducer from "./features/reglement/slice";
import {
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist';
const appReducer = combineReducers({
client: clientReducer,
article: articleReducer,
devis: devisReducer,
commande: commandeReducer,
facture: factureReducer,
BL: BLReducer,
fournisseur: fournisseurReducer,
avoir: avoirReducer,
famille: familleReducer,
contact: contactReducer,
gateways: gatewaysReducer,
commercials: commercialsReducer,
universign: universignReducer,
user: userReducer,
entreprise: entrepriseReducer,
sageBuilder: sageBuilderReducer,
reglement: reglementReducer
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rootReducer = (state: any, action: any) => {
if (action.type === 'RESET_APP') {
state = undefined;
}
return appReducer(state, action);
};
const persistConfig = {
key: 'root',
storage,
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
});
export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

4
src/types.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module "*.svg" {
const content: string;
export default content;
}

Some files were not shown because too many files have changed in this diff Show more