Compare commits
No commits in common. "c4edfbd6de8d14bbeeee4afda44122771a088ebd" and "8ee7b8a1775c8c286935b58f7096542e5b370932" have entirely different histories.
c4edfbd6de
...
8ee7b8a177
@ -56,8 +56,6 @@ import Iframe from "@/components/4nk/Iframe"
|
|||||||
import MessageBus from "@/lib/4nk/MessageBus"
|
import MessageBus from "@/lib/4nk/MessageBus"
|
||||||
import EventBus from "@/lib/4nk/EventBus"
|
import EventBus from "@/lib/4nk/EventBus"
|
||||||
import UserStore from "@/lib/4nk/UserStore"
|
import UserStore from "@/lib/4nk/UserStore"
|
||||||
import ProcessesViewer from "@/components/ProcessesViewer"
|
|
||||||
import { iframeUrl } from "@/app/page"
|
|
||||||
|
|
||||||
interface FolderData {
|
interface FolderData {
|
||||||
id: number
|
id: number
|
||||||
@ -153,8 +151,6 @@ interface Role {
|
|||||||
level: "folder" | "space" | "global"
|
level: "folder" | "space" | "global"
|
||||||
}
|
}
|
||||||
|
|
||||||
type FolderType = "contrat" | "projet" | "rapport" | "finance" | "rh" | "marketing";
|
|
||||||
|
|
||||||
export default function FoldersPage() {
|
export default function FoldersPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [viewMode, setViewMode] = useState<'list'>('list')
|
const [viewMode, setViewMode] = useState<'list'>('list')
|
||||||
@ -169,9 +165,6 @@ export default function FoldersPage() {
|
|||||||
const [currentPath, setCurrentPath] = useState<string[]>(["Racine"])
|
const [currentPath, setCurrentPath] = useState<string[]>(["Racine"])
|
||||||
const [actionModal, setActionModal] = useState<ActionModal>({ type: null, folder: null, folders: [] })
|
const [actionModal, setActionModal] = useState<ActionModal>({ type: null, folder: null, folders: [] })
|
||||||
const [showCreateFolderModal, setShowCreateFolderModal] = useState(false)
|
const [showCreateFolderModal, setShowCreateFolderModal] = useState(false)
|
||||||
const [folderType, setFolderType] = useState<FolderType | null>(null);
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
|
||||||
|
|
||||||
// 4NK Integration states
|
// 4NK Integration states
|
||||||
const [isConnected, setIsConnected] = useState(false)
|
const [isConnected, setIsConnected] = useState(false)
|
||||||
@ -180,6 +173,7 @@ export default function FoldersPage() {
|
|||||||
const [myProcesses, setMyProcesses] = useState<string[]>([])
|
const [myProcesses, setMyProcesses] = useState<string[]>([])
|
||||||
const [userPairingId, setUserPairingId] = useState<string | null>(null)
|
const [userPairingId, setUserPairingId] = useState<string | null>(null)
|
||||||
const [pairingIdInitialized, setPairingIdInitialized] = useState(false)
|
const [pairingIdInitialized, setPairingIdInitialized] = useState(false)
|
||||||
|
const iframeUrl = 'https://dev3.4nkweb.com'
|
||||||
|
|
||||||
// Modal states
|
// Modal states
|
||||||
const [inviteMessage, setInviteMessage] = useState("")
|
const [inviteMessage, setInviteMessage] = useState("")
|
||||||
@ -361,7 +355,7 @@ export default function FoldersPage() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isConnected, iframeUrl]);
|
}, [isConnected]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isConnected && processes !== null) {
|
if (isConnected && processes !== null) {
|
||||||
@ -714,7 +708,7 @@ export default function FoldersPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadFolders()
|
loadFolders()
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
// Notification system
|
// Notification system
|
||||||
const showNotification = (type: "success" | "error" | "info", message: string) => {
|
const showNotification = (type: "success" | "error" | "info", message: string) => {
|
||||||
@ -741,7 +735,6 @@ export default function FoldersPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAuthConnect = useCallback(() => {
|
const handleAuthConnect = useCallback(() => {
|
||||||
setIsConnected(true);
|
|
||||||
setShowAuthModal(false);
|
setShowAuthModal(false);
|
||||||
console.log('Auth Connect - Connexion établie, le useEffect se chargera de récupérer le userPairingId');
|
console.log('Auth Connect - Connexion établie, le useEffect se chargera de récupérer le userPairingId');
|
||||||
showNotification("success", "Connexion 4NK réussie");
|
showNotification("success", "Connexion 4NK réussie");
|
||||||
@ -967,65 +960,52 @@ export default function FoldersPage() {
|
|||||||
setActionModal({ type: "delete", folder, folders: [] })
|
setActionModal({ type: "delete", folder, folders: [] })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenModal = (type: FolderType) => {
|
const handleCreateFolder = () => {
|
||||||
setFolderType(type);
|
setShowCreateFolderModal(true)
|
||||||
setIsModalOpen(true);
|
|
||||||
setMenuOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
|
||||||
setIsModalOpen(false);
|
|
||||||
setFolderType(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveNewFolder = useCallback(
|
|
||||||
(folderData: SDKFolderData) => {
|
|
||||||
if (!isConnected || !userPairingId) {
|
|
||||||
console.error('Conditions non remplies:', { isConnected, userPairingId });
|
|
||||||
showNotification(
|
|
||||||
"error",
|
|
||||||
`Vous devez être connecté à 4NK pour créer un dossier (Connected: ${isConnected}, PairingId: ${userPairingId ? 'OK' : 'NULL'})`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajout du type dans les données du dossier
|
const handleSaveNewFolder = useCallback((folderData: SDKFolderData) => {
|
||||||
const folderToCreate = {
|
console.log('Debug - handleSaveNewFolder:', {
|
||||||
...folderData,
|
isConnected,
|
||||||
type: folderType
|
userPairingId,
|
||||||
};
|
userPairingIdType: typeof userPairingId,
|
||||||
|
userStoreConnected: UserStore.getInstance().isConnected(),
|
||||||
|
userStorePairingId: UserStore.getInstance().getUserPairingId()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userPairingId !== null && isConnected) {
|
||||||
const roles = setDefaultFolderRoles(userPairingId, [], []);
|
const roles = setDefaultFolderRoles(userPairingId, [], []);
|
||||||
const folderPrivateFields = FolderPrivateFields;
|
const folderPrivateFields = FolderPrivateFields;
|
||||||
|
|
||||||
MessageBus.getInstance(iframeUrl)
|
MessageBus.getInstance(iframeUrl).createFolder(folderData, folderPrivateFields, roles).then((_folderCreated: FolderCreated) => {
|
||||||
.createFolder(folderToCreate, folderPrivateFields, roles)
|
MessageBus.getInstance(iframeUrl).notifyProcessUpdate(_folderCreated.processId, _folderCreated.process.states[0].state_id).then(() => {
|
||||||
.then((_folderCreated: FolderCreated) => {
|
MessageBus.getInstance(iframeUrl).validateState(_folderCreated.processId, _folderCreated.process.states[0].state_id).then((_updatedProcess: any) => {
|
||||||
const firstStateId = _folderCreated.process.states[0].state_id;
|
MessageBus.getInstance(iframeUrl).getProcesses().then((processes: any) => {
|
||||||
MessageBus.getInstance(iframeUrl)
|
setProcesses(processes);
|
||||||
.notifyProcessUpdate(_folderCreated.processId, firstStateId)
|
});
|
||||||
.then(() =>
|
});
|
||||||
MessageBus.getInstance(iframeUrl)
|
|
||||||
.validateState(_folderCreated.processId, firstStateId)
|
|
||||||
.then(() =>
|
|
||||||
MessageBus.getInstance(iframeUrl)
|
|
||||||
.getProcesses()
|
|
||||||
.then(async (processes: any) => {
|
|
||||||
setProcesses(processes)
|
|
||||||
})
|
})
|
||||||
)
|
}).catch((error) => {
|
||||||
);
|
|
||||||
|
|
||||||
setShowCreateFolderModal(false);
|
|
||||||
showNotification("success", `Dossier "${folderData.name}" créé avec succès sur 4NK`);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Erreur lors de la création du dossier 4NK:', error);
|
console.error('Erreur lors de la création du dossier 4NK:', error);
|
||||||
showNotification("error", "Erreur lors de la création du dossier");
|
showNotification("error", "Erreur lors de la création du dossier");
|
||||||
});
|
});
|
||||||
},
|
|
||||||
[userPairingId, isConnected, iframeUrl, folderType]
|
setShowCreateFolderModal(false);
|
||||||
);
|
showNotification("success", `Dossier "${folderData.name}" créé avec succès sur 4NK`);
|
||||||
|
} else {
|
||||||
|
console.error('Conditions non remplies:', {
|
||||||
|
userPairingIdCheck: userPairingId !== null,
|
||||||
|
isConnectedCheck: isConnected,
|
||||||
|
actualUserPairingId: userPairingId,
|
||||||
|
actualIsConnected: isConnected
|
||||||
|
});
|
||||||
|
showNotification("error", `Vous devez être connecté à 4NK pour créer un dossier (Connected: ${isConnected}, PairingId: ${userPairingId ? 'OK' : 'NULL'})`);
|
||||||
|
}
|
||||||
|
}, [userPairingId, isConnected, iframeUrl]);
|
||||||
|
|
||||||
|
const handleCancelCreateFolder = () => {
|
||||||
|
setShowCreateFolderModal(false)
|
||||||
|
}
|
||||||
|
|
||||||
const handleToggleFavorite = (folderId: number) => {
|
const handleToggleFavorite = (folderId: number) => {
|
||||||
const folder = folders.find((f) => f.id === folderId)
|
const folder = folders.find((f) => f.id === folderId)
|
||||||
@ -1471,7 +1451,8 @@ export default function FoldersPage() {
|
|||||||
{/* Notification */}
|
{/* Notification */}
|
||||||
{notification && (
|
{notification && (
|
||||||
<div
|
<div
|
||||||
className={`fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg flex items-center space-x-2 ${notification.type === "success"
|
className={`fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg flex items-center space-x-2 ${
|
||||||
|
notification.type === "success"
|
||||||
? "bg-green-100 text-green-800 border border-green-200"
|
? "bg-green-100 text-green-800 border border-green-200"
|
||||||
: notification.type === "error"
|
: notification.type === "error"
|
||||||
? "bg-red-100 text-red-800 border border-red-200"
|
? "bg-red-100 text-red-800 border border-red-200"
|
||||||
@ -1514,45 +1495,23 @@ export default function FoldersPage() {
|
|||||||
<Upload className="h-4 w-4 mr-2" />
|
<Upload className="h-4 w-4 mr-2" />
|
||||||
Importer
|
Importer
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{isConnected ? (
|
{isConnected ? (
|
||||||
<div className="flex gap-2">
|
<>
|
||||||
{/* Nouveau dossier avec menu */}
|
<Button size="sm" onClick={handleCreateFolder}>
|
||||||
<div className="relative">
|
|
||||||
<Button size="sm" onClick={() => setMenuOpen(!menuOpen)}>
|
|
||||||
<FolderPlus className="h-4 w-4 mr-2" />
|
<FolderPlus className="h-4 w-4 mr-2" />
|
||||||
Nouveau dossier
|
Nouveau dossier
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{menuOpen && (
|
|
||||||
<div className="absolute mt-1 right-0 w-48 bg-white border border-gray-200 rounded shadow-lg z-50">
|
|
||||||
{['contrat', 'projet', 'rapport', 'finance', 'rh', 'marketing'].map((type) => (
|
|
||||||
<button
|
|
||||||
key={type}
|
|
||||||
className="w-full text-left px-4 py-2 text-gray-700 hover:bg-gray-100"
|
|
||||||
onClick={() => handleOpenModal(type as FolderType)}
|
|
||||||
>
|
|
||||||
{type.charAt(0).toUpperCase() + type.slice(1)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Déconnexion */}
|
|
||||||
<Button variant="outline" size="sm" onClick={handleLogout}>
|
<Button variant="outline" size="sm" onClick={handleLogout}>
|
||||||
<X className="h-4 w-4 mr-2" />
|
<X className="h-4 w-4 mr-2" />
|
||||||
Déconnexion 4NK
|
Déconnexion 4NK
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Debug PairingId */}
|
|
||||||
{!userPairingId && (
|
{!userPairingId && (
|
||||||
<Button variant="outline" size="sm" onClick={handleForceGetPairingId}>
|
<Button variant="outline" size="sm" onClick={handleForceGetPairingId}>
|
||||||
<Brain className="h-4 w-4 mr-2" />
|
<Brain className="h-4 w-4 mr-2" />
|
||||||
Debug PairingId
|
Debug PairingId
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Button variant="outline" size="sm" onClick={handleLogin}>
|
<Button variant="outline" size="sm" onClick={handleLogin}>
|
||||||
<Shield className="h-4 w-4 mr-2" />
|
<Shield className="h-4 w-4 mr-2" />
|
||||||
@ -1622,6 +1581,7 @@ export default function FoldersPage() {
|
|||||||
{sortOrder === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />}
|
{sortOrder === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Vue grille supprimée: forcer la vue liste uniquement */}
|
{/* Vue grille supprimée: forcer la vue liste uniquement */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1811,7 +1771,7 @@ export default function FoldersPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Folders List/Grid */}
|
{/* Folders List/Grid */}
|
||||||
{/* <Card>
|
<Card>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
{viewMode === "list" ? (
|
{viewMode === "list" ? (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@ -1887,7 +1847,8 @@ export default function FoldersPage() {
|
|||||||
{filteredFolders.map((folder) => (
|
{filteredFolders.map((folder) => (
|
||||||
<div
|
<div
|
||||||
key={folder.id}
|
key={folder.id}
|
||||||
className={`relative group border rounded-lg p-6 hover:shadow-md transition-shadow cursor-pointer ${selectedFolders.includes(folder.id) ? "bg-blue-50 border-blue-200" : "bg-white"
|
className={`relative group border rounded-lg p-6 hover:shadow-md transition-shadow cursor-pointer ${
|
||||||
|
selectedFolders.includes(folder.id) ? "bg-blue-50 border-blue-200" : "bg-white"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleOpenFolder(folder)}
|
onClick={() => handleOpenFolder(folder)}
|
||||||
>
|
>
|
||||||
@ -1987,7 +1948,7 @@ export default function FoldersPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Recent Activity
|
{/* Recent Activity */}
|
||||||
<div className="mt-4 pt-4 border-t">
|
<div className="mt-4 pt-4 border-t">
|
||||||
<h4 className="text-xs font-medium text-gray-700 mb-2">Activité récente</h4>
|
<h4 className="text-xs font-medium text-gray-700 mb-2">Activité récente</h4>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@ -2015,25 +1976,13 @@ export default function FoldersPage() {
|
|||||||
? "Essayez de modifier vos critères de recherche"
|
? "Essayez de modifier vos critères de recherche"
|
||||||
: "Commencez par créer votre premier dossier"}
|
: "Commencez par créer votre premier dossier"}
|
||||||
</p>
|
</p>
|
||||||
|
<Button onClick={handleCreateFolder}>
|
||||||
|
<FolderPlus className="h-4 w-4 mr-2" />
|
||||||
|
Nouveau dossier
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card> */}
|
|
||||||
|
|
||||||
{/* ProcessesViewer Card */}
|
|
||||||
<Card className="mt-6">
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Processus Blockchain</h3>
|
|
||||||
|
|
||||||
{/* Intégration du ProcessesViewer */}
|
|
||||||
<div className="w-full h-[500px]">
|
|
||||||
<ProcessesViewer
|
|
||||||
processes={processes}
|
|
||||||
myProcesses={myProcesses}
|
|
||||||
onProcessesUpdate={setProcesses}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
@ -2599,16 +2548,13 @@ export default function FoldersPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Modal */}
|
{/* Folder Creation Modal */}
|
||||||
{folderType && (
|
|
||||||
<FolderModal
|
<FolderModal
|
||||||
isOpen={isModalOpen}
|
isOpen={showCreateFolderModal}
|
||||||
onClose={handleCloseModal}
|
onClose={handleCancelCreateFolder}
|
||||||
onSave={handleSaveNewFolder}
|
onSave={handleSaveNewFolder}
|
||||||
onCancel={handleCloseModal}
|
onCancel={handleCancelCreateFolder}
|
||||||
folderType={folderType}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 4NK Authentication Modal */}
|
{/* 4NK Authentication Modal */}
|
||||||
<AuthModal
|
<AuthModal
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import MessageBus from "@/lib/4nk/MessageBus"
|
|||||||
import UserStore from "@/lib/4nk/UserStore"
|
import UserStore from "@/lib/4nk/UserStore"
|
||||||
import Iframe from "@/components/4nk/Iframe"
|
import Iframe from "@/components/4nk/Iframe"
|
||||||
import EventBus from "@/lib/4nk/EventBus"
|
import EventBus from "@/lib/4nk/EventBus"
|
||||||
import { iframeUrl } from "../page"
|
// DebugInfo supprimé
|
||||||
|
|
||||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||||
@ -46,6 +46,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev3.4nkweb.com"
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: "Tableau de bord", href: "/dashboard", icon: LayoutDashboard },
|
{ name: "Tableau de bord", href: "/dashboard", icon: LayoutDashboard },
|
||||||
@ -84,7 +85,6 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
messageBus.isReady().then(() => {
|
messageBus.isReady().then(() => {
|
||||||
messageBus.getMyProcesses().then((res: string[]) => {
|
messageBus.getMyProcesses().then((res: string[]) => {
|
||||||
setMyProcesses(res);
|
setMyProcesses(res);
|
||||||
console.log("getMyProcesses", res);
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -168,6 +168,8 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
<Shield className="h-12 w-12 mx-auto mb-4 text-blue-600 animate-pulse" />
|
<Shield className="h-12 w-12 mx-auto mb-4 text-blue-600 animate-pulse" />
|
||||||
<p className="text-gray-600">Vérification de l'authentification...</p>
|
<p className="text-gray-600">Vérification de l'authentification...</p>
|
||||||
</div>
|
</div>
|
||||||
|
{<Iframe iframeUrl={iframeUrl} />}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -349,7 +351,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isConnected && <Iframe iframeUrl={iframeUrl} />}
|
{<Iframe iframeUrl={iframeUrl} />}
|
||||||
|
|
||||||
{/* Debug info retiré */}
|
{/* Debug info retiré */}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
HardDrive,
|
HardDrive,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
import MessageBus from "@/lib/4nk/MessageBus"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
@ -53,6 +54,11 @@ export default function DashboardPage() {
|
|||||||
const [notifications, setNotifications] = useState<any[]>([])
|
const [notifications, setNotifications] = useState<any[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev3.4nkweb.com"
|
||||||
|
const messageBus = MessageBus.getInstance(iframeUrl)
|
||||||
|
// const mockMode = messageBus.isInMockMode()
|
||||||
|
// setIsMockMode(mockMode)
|
||||||
|
|
||||||
// Simuler le chargement des données
|
// Simuler le chargement des données
|
||||||
if (true) {
|
if (true) {
|
||||||
setStats({
|
setStats({
|
||||||
|
|||||||
36
app/page.tsx
36
app/page.tsx
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@ -8,25 +8,16 @@ import { Badge } from "@/components/ui/badge"
|
|||||||
import { Shield, ArrowRight, Key, Zap, Users, Globe, Database, Code, CheckCircle } from "lucide-react"
|
import { Shield, ArrowRight, Key, Zap, Users, Globe, Database, Code, CheckCircle } from "lucide-react"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import AuthModal from "@/components/4nk/AuthModal"
|
import AuthModal from "@/components/4nk/AuthModal"
|
||||||
|
import Iframe from "@/components/4nk/Iframe"
|
||||||
export const iframeUrl = 'https://dev3.4nkweb.com'
|
import UserStore from "@/lib/4nk/UserStore"
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [showAuthModal, setShowAuthModal] = useState(false)
|
const [showLoginModal, setShowLoginModal] = useState(false)
|
||||||
const [isConnected, setIsConnected] = useState(false)
|
const [isConnected, setIsConnected] = useState(false)
|
||||||
|
const [userPairingId, setUserPairingId] = useState<string | null>(null)
|
||||||
const handleAuthConnect = useCallback(() => {
|
|
||||||
setIsConnected(true);
|
|
||||||
setShowAuthModal(false);
|
|
||||||
router.push("/dashboard")
|
|
||||||
console.log('Auth Connect - Connexion établie, le useEffect se chargera de récupérer le userPairingId');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleAuthClose = useCallback(() => {
|
|
||||||
setShowAuthModal(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev3.4nkweb.com"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
|
||||||
@ -53,7 +44,7 @@ export default function HomePage() {
|
|||||||
<Link href="/formation">
|
<Link href="/formation">
|
||||||
<Button variant="outline">Formation</Button>
|
<Button variant="outline">Formation</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button onClick={() => setShowAuthModal(true)}>Connexion</Button>
|
<Button onClick={() => setShowLoginModal(true)}>Connexion</Button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -85,15 +76,20 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Modal d’authentification */}
|
{/* Modal d’authentification */}
|
||||||
{showAuthModal && (
|
{showLoginModal && (
|
||||||
<AuthModal
|
<AuthModal
|
||||||
isOpen={showAuthModal}
|
isOpen={showLoginModal}
|
||||||
onConnect={handleAuthConnect}
|
onConnect={() => {
|
||||||
onClose={handleAuthClose}
|
setShowLoginModal(false)
|
||||||
|
router.push("/dashboard") // ✅ redirection après login
|
||||||
|
}}
|
||||||
|
onClose={() => setShowLoginModal(false)}
|
||||||
iframeUrl={iframeUrl}
|
iframeUrl={iframeUrl}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{<Iframe iframeUrl={iframeUrl} />}
|
||||||
|
|
||||||
{/* Product Features */}
|
{/* Product Features */}
|
||||||
<section id="produit" className="py-16 px-4 bg-white">
|
<section id="produit" className="py-16 px-4 bg-white">
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useState, useEffect, memo } from 'react';
|
|||||||
import Iframe from './Iframe';
|
import Iframe from './Iframe';
|
||||||
import MessageBus from '@/lib/4nk/MessageBus';
|
import MessageBus from '@/lib/4nk/MessageBus';
|
||||||
import Loader from '@/lib/4nk/Loader';
|
import Loader from '@/lib/4nk/Loader';
|
||||||
import Modal from '../ui/modal/Modal';
|
import Modal from '../modal/Modal';
|
||||||
|
|
||||||
interface AuthModalProps {
|
interface AuthModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -52,63 +52,48 @@ function AuthModal({ isOpen, onConnect, onClose, iframeUrl }: AuthModalProps) {
|
|||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title="Authentification 4nk"
|
title='Authentification 4nk'
|
||||||
size="md"
|
|
||||||
>
|
|
||||||
{/* Loader affiché tant que l'iframe n'est pas prête */}
|
|
||||||
{!isIframeReady && !authSuccess && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
height: "400px",
|
|
||||||
gap: 16,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
{!isIframeReady && (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '400px',
|
||||||
|
gap: 16
|
||||||
|
}}>
|
||||||
<Loader width={40} />
|
<Loader width={40} />
|
||||||
<div style={{ fontWeight: 600, fontSize: 18 }}>
|
<div style={{ fontWeight: 600, fontSize: 18 }}>Chargement de l'authentification...</div>
|
||||||
Chargement de l'authentification...
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{authSuccess ? (
|
||||||
{/* Message de succès */}
|
<div style={{
|
||||||
{authSuccess && (
|
display: 'flex',
|
||||||
<div
|
flexDirection: 'column',
|
||||||
style={{
|
alignItems: 'center',
|
||||||
display: "flex",
|
justifyContent: 'center',
|
||||||
flexDirection: "column",
|
height: '400px',
|
||||||
alignItems: "center",
|
gap: 20
|
||||||
justifyContent: "center",
|
}}>
|
||||||
height: "400px",
|
<div style={{ fontWeight: 600, fontSize: 18, color: '#43a047' }}>
|
||||||
gap: 20,
|
Authentification réussie !
|
||||||
animation: "fadeInSuccess 0.4s ease-out",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontWeight: 600, fontSize: 18, color: "#43a047" }}>
|
|
||||||
✅ Authentification réussie !
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{
|
||||||
|
display: showIframe ? 'flex' : 'none',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%'
|
||||||
|
}}>
|
||||||
|
<Iframe
|
||||||
|
iframeUrl={iframeUrl}
|
||||||
|
showIframe={showIframe}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</Modal>
|
||||||
{/* Iframe affichée uniquement si dispo */}
|
|
||||||
{!authSuccess && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: showIframe ? "flex" : "none",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "100%",
|
|
||||||
minHeight: "400px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Iframe iframeUrl={iframeUrl} showIframe={showIframe} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,17 @@
|
|||||||
/* Container */
|
/* Folder Modal Styles */
|
||||||
.folder-container {
|
.folder-container {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form */
|
|
||||||
.folder-form {
|
.folder-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Section */
|
/* Form Sections */
|
||||||
.form-section {
|
.form-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -24,13 +21,13 @@
|
|||||||
.section-title {
|
.section-title {
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #111827;
|
color: #374151;
|
||||||
margin: 0;
|
margin: 0 0 0.5rem 0;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
border-bottom: 2px solid #e5e7eb;
|
border-bottom: 2px solid #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Layout */
|
/* Form Layout */
|
||||||
.form-row {
|
.form-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
@ -40,44 +37,43 @@
|
|||||||
.form-field {
|
.form-field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.4rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field label {
|
.form-field label {
|
||||||
font-size: 0.85rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
}
|
}
|
||||||
|
|
||||||
.required {
|
.required {
|
||||||
color: #dc2626;
|
color: #dc2626;
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inputs */
|
/* Form Inputs */
|
||||||
.form-field input,
|
.form-field input,
|
||||||
.form-field textarea,
|
.form-field textarea,
|
||||||
.form-field select {
|
.form-field select {
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border: 1px solid #d1d5db;
|
border: 1px solid #d1d5db;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.875rem;
|
||||||
transition: border-color 0.2s, box-shadow 0.2s;
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
background-color: #fff;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field input:focus,
|
.form-field input:focus,
|
||||||
.form-field textarea:focus,
|
.form-field textarea:focus,
|
||||||
.form-field select:focus {
|
.form-field select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #2563eb;
|
border-color: #3b82f6;
|
||||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field input:disabled,
|
.form-field input:disabled,
|
||||||
.form-field textarea:disabled,
|
.form-field textarea:disabled,
|
||||||
.form-field select:disabled {
|
.form-field select:disabled {
|
||||||
background-color: #f3f4f6;
|
background-color: #f9fafb;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
@ -87,7 +83,7 @@
|
|||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tags */
|
/* Tag System */
|
||||||
.tag-list {
|
.tag-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -98,25 +94,24 @@
|
|||||||
.tag-item {
|
.tag-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.5rem;
|
||||||
background-color: #f0f9ff;
|
background-color: #eff6ff;
|
||||||
color: #0369a1;
|
color: #1d4ed8;
|
||||||
padding: 0.35rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.875rem;
|
||||||
border: 1px solid #bae6fd;
|
border: 1px solid #bfdbfe;
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-remove {
|
.tag-remove {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 1.1rem;
|
width: 1.25rem;
|
||||||
height: 1.1rem;
|
height: 1.25rem;
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
color: #0369a1;
|
color: #1d4ed8;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@ -125,7 +120,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag-remove:hover {
|
.tag-remove:hover {
|
||||||
background-color: #e0f2fe;
|
background-color: #dbeafe;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-input-container {
|
.tag-input-container {
|
||||||
@ -139,12 +134,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-add-tag {
|
.btn-add-tag {
|
||||||
padding: 0.6rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
background-color: #2563eb;
|
background-color: #3b82f6;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
@ -152,10 +147,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-add-tag:hover {
|
.btn-add-tag:hover {
|
||||||
background-color: #1d4ed8;
|
background-color: #2563eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Actions */
|
/* Form Actions */
|
||||||
.form-actions {
|
.form-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@ -165,21 +160,16 @@
|
|||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-cancel,
|
|
||||||
.btn-submit {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cancel button */
|
|
||||||
.btn-cancel {
|
.btn-cancel {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
border: 1px solid #d1d5db;
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-cancel:hover {
|
.btn-cancel:hover {
|
||||||
@ -187,15 +177,20 @@
|
|||||||
border-color: #9ca3af;
|
border-color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Submit button */
|
|
||||||
.btn-submit {
|
.btn-submit {
|
||||||
background-color: #10b981;
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: #059669;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-submit:hover {
|
.btn-submit:hover {
|
||||||
background-color: #059669;
|
background-color: #047857;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-submit:disabled {
|
.btn-submit:disabled {
|
||||||
@ -203,7 +198,7 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive Design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.folder-container {
|
.folder-container {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@ -229,7 +224,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scrollbar */
|
/* Custom scrollbar for the container */
|
||||||
.folder-container::-webkit-scrollbar {
|
.folder-container::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
@ -247,101 +242,3 @@
|
|||||||
.folder-container::-webkit-scrollbar-thumb:hover {
|
.folder-container::-webkit-scrollbar-thumb:hover {
|
||||||
background: #94a3b8;
|
background: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Thèmes par type de dossier --- */
|
|
||||||
|
|
||||||
/* Contrats */
|
|
||||||
.folder-contrat .section-title {
|
|
||||||
border-bottom-color: #2563eb;
|
|
||||||
}
|
|
||||||
.folder-contrat .tag-item {
|
|
||||||
background-color: #eff6ff;
|
|
||||||
color: #2563eb;
|
|
||||||
border-color: #bfdbfe;
|
|
||||||
}
|
|
||||||
.folder-contrat .btn-submit {
|
|
||||||
background-color: #2563eb;
|
|
||||||
}
|
|
||||||
.folder-contrat .btn-submit:hover {
|
|
||||||
background-color: #1d4ed8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Projets */
|
|
||||||
.folder-projet .section-title {
|
|
||||||
border-bottom-color: #059669;
|
|
||||||
}
|
|
||||||
.folder-projet .tag-item {
|
|
||||||
background-color: #ecfdf5;
|
|
||||||
color: #059669;
|
|
||||||
border-color: #a7f3d0;
|
|
||||||
}
|
|
||||||
.folder-projet .btn-submit {
|
|
||||||
background-color: #059669;
|
|
||||||
}
|
|
||||||
.folder-projet .btn-submit:hover {
|
|
||||||
background-color: #047857;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Rapports */
|
|
||||||
.folder-rapport .section-title {
|
|
||||||
border-bottom-color: #7c3aed;
|
|
||||||
}
|
|
||||||
.folder-rapport .tag-item {
|
|
||||||
background-color: #f5f3ff;
|
|
||||||
color: #7c3aed;
|
|
||||||
border-color: #ddd6fe;
|
|
||||||
}
|
|
||||||
.folder-rapport .btn-submit {
|
|
||||||
background-color: #7c3aed;
|
|
||||||
}
|
|
||||||
.folder-rapport .btn-submit:hover {
|
|
||||||
background-color: #6d28d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Finance */
|
|
||||||
.folder-finance .section-title {
|
|
||||||
border-bottom-color: #d97706;
|
|
||||||
}
|
|
||||||
.folder-finance .tag-item {
|
|
||||||
background-color: #fffbeb;
|
|
||||||
color: #d97706;
|
|
||||||
border-color: #fcd34d;
|
|
||||||
}
|
|
||||||
.folder-finance .btn-submit {
|
|
||||||
background-color: #d97706;
|
|
||||||
}
|
|
||||||
.folder-finance .btn-submit:hover {
|
|
||||||
background-color: #b45309;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ressources Humaines */
|
|
||||||
.folder-rh .section-title {
|
|
||||||
border-bottom-color: #db2777;
|
|
||||||
}
|
|
||||||
.folder-rh .tag-item {
|
|
||||||
background-color: #fdf2f8;
|
|
||||||
color: #db2777;
|
|
||||||
border-color: #f9a8d4;
|
|
||||||
}
|
|
||||||
.folder-rh .btn-submit {
|
|
||||||
background-color: #db2777;
|
|
||||||
}
|
|
||||||
.folder-rh .btn-submit:hover {
|
|
||||||
background-color: #be185d;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Marketing */
|
|
||||||
.folder-marketing .section-title {
|
|
||||||
border-bottom-color: #4f46e5;
|
|
||||||
}
|
|
||||||
.folder-marketing .tag-item {
|
|
||||||
background-color: #eef2ff;
|
|
||||||
color: #4f46e5;
|
|
||||||
border-color: #c7d2fe;
|
|
||||||
}
|
|
||||||
.folder-marketing .btn-submit {
|
|
||||||
background-color: #4f46e5;
|
|
||||||
}
|
|
||||||
.folder-marketing .btn-submit:hover {
|
|
||||||
background-color: #3730a3;
|
|
||||||
}
|
|
||||||
@ -1,10 +1,8 @@
|
|||||||
import React, { useEffect, useState, memo } from 'react';
|
import React, { useState, memo } from 'react';
|
||||||
import Modal from './ui/modal/Modal';
|
import Modal from './ui/modal/Modal';
|
||||||
import './FolderModal.css';
|
import './FolderModal.css';
|
||||||
import type { FolderData } from '../lib/4nk/models/FolderData';
|
import type { FolderData } from '../lib/4nk/models/FolderData';
|
||||||
|
|
||||||
type FolderType = 'contrat' | 'projet' | 'rapport' | 'finance' | 'rh' | 'marketing' | 'autre';
|
|
||||||
|
|
||||||
interface FolderModalProps {
|
interface FolderModalProps {
|
||||||
folder?: FolderData;
|
folder?: FolderData;
|
||||||
onSave?: (folderData: FolderData) => void;
|
onSave?: (folderData: FolderData) => void;
|
||||||
@ -12,11 +10,6 @@ interface FolderModalProps {
|
|||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
folderType?: FolderType;
|
|
||||||
renderExtraFields?: (
|
|
||||||
folderData: FolderData,
|
|
||||||
setFolderData: React.Dispatch<React.SetStateAction<FolderData>>
|
|
||||||
) => React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultFolder: FolderData = {
|
const defaultFolder: FolderData = {
|
||||||
@ -34,92 +27,80 @@ const defaultFolder: FolderData = {
|
|||||||
stakeholders: []
|
stakeholders: []
|
||||||
};
|
};
|
||||||
|
|
||||||
function capitalize(s?: string) {
|
|
||||||
if (!s) return '';
|
|
||||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FolderModal({
|
function FolderModal({
|
||||||
folder = defaultFolder,
|
folder = defaultFolder,
|
||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose
|
||||||
folderType = 'autre',
|
|
||||||
renderExtraFields
|
|
||||||
}: FolderModalProps) {
|
}: FolderModalProps) {
|
||||||
const [folderData, setFolderData] = useState<FolderData>({ ...defaultFolder, ...folder });
|
const [folderData, setFolderData] = useState<FolderData>(folder);
|
||||||
const [currentCustomer, setCurrentCustomer] = useState<string>('');
|
const [currentCustomer, setCurrentCustomer] = useState<string>('');
|
||||||
const [currentStakeholder, setCurrentStakeholder] = useState<string>('');
|
const [currentStakeholder, setCurrentStakeholder] = useState<string>('');
|
||||||
const [currentNote, setCurrentNote] = useState<string>('');
|
const [currentNote, setCurrentNote] = useState<string>('');
|
||||||
|
|
||||||
// Sync when modal opens or when folder prop changes (useful pour Edit)
|
if (!isOpen) return null;
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
// Merge with defaultFolder to ensure arrays exist
|
|
||||||
setFolderData({ ...defaultFolder, ...(folder || {}) });
|
|
||||||
setCurrentCustomer('');
|
|
||||||
setCurrentStakeholder('');
|
|
||||||
setCurrentNote('');
|
|
||||||
}
|
|
||||||
}, [isOpen, folder]);
|
|
||||||
|
|
||||||
// Generic input change handler
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||||
const handleInputChange = (
|
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
|
||||||
) => {
|
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
// cast to avoid TS complaints when updating dynamic fields
|
setFolderData(prev => ({
|
||||||
setFolderData(prev => ({ ...(prev as any), [name]: value } as FolderData));
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ---------- Customers ---------- */
|
|
||||||
const addCustomer = () => {
|
const addCustomer = () => {
|
||||||
const v = currentCustomer.trim();
|
if (currentCustomer.trim() && !folderData.customers.includes(currentCustomer.trim())) {
|
||||||
if (!v) return;
|
setFolderData(prev => ({
|
||||||
if (!Array.isArray(folderData.customers)) folderData.customers = [];
|
...prev,
|
||||||
if (!folderData.customers.includes(v)) {
|
customers: [...prev.customers, currentCustomer.trim()]
|
||||||
setFolderData(prev => ({ ...prev, customers: [...(prev.customers || []), v] }));
|
}));
|
||||||
}
|
|
||||||
setCurrentCustomer('');
|
setCurrentCustomer('');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeCustomer = (customer: string) => {
|
const removeCustomer = (customer: string) => {
|
||||||
setFolderData(prev => ({ ...prev, customers: (prev.customers || []).filter(c => c !== customer) }));
|
setFolderData(prev => ({
|
||||||
|
...prev,
|
||||||
|
customers: prev.customers.filter(c => c !== customer)
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ---------- Stakeholders ---------- */
|
|
||||||
const addStakeholder = () => {
|
const addStakeholder = () => {
|
||||||
const v = currentStakeholder.trim();
|
if (currentStakeholder.trim() && !folderData.stakeholders.includes(currentStakeholder.trim())) {
|
||||||
if (!v) return;
|
setFolderData(prev => ({
|
||||||
if (!Array.isArray(folderData.stakeholders)) folderData.stakeholders = [];
|
...prev,
|
||||||
if (!folderData.stakeholders.includes(v)) {
|
stakeholders: [...prev.stakeholders, currentStakeholder.trim()]
|
||||||
setFolderData(prev => ({ ...prev, stakeholders: [...(prev.stakeholders || []), v] }));
|
}));
|
||||||
}
|
|
||||||
setCurrentStakeholder('');
|
setCurrentStakeholder('');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeStakeholder = (stakeholder: string) => {
|
const removeStakeholder = (stakeholder: string) => {
|
||||||
setFolderData(prev => ({ ...prev, stakeholders: (prev.stakeholders || []).filter(s => s !== stakeholder) }));
|
setFolderData(prev => ({
|
||||||
|
...prev,
|
||||||
|
stakeholders: prev.stakeholders.filter(s => s !== stakeholder)
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ---------- Notes ---------- */
|
|
||||||
const addNote = () => {
|
const addNote = () => {
|
||||||
const v = currentNote.trim();
|
if (currentNote.trim() && !folderData.notes.includes(currentNote.trim())) {
|
||||||
if (!v) return;
|
setFolderData(prev => ({
|
||||||
if (!Array.isArray(folderData.notes)) folderData.notes = [];
|
...prev,
|
||||||
if (!folderData.notes.includes(v)) {
|
notes: [...prev.notes, currentNote.trim()]
|
||||||
setFolderData(prev => ({ ...prev, notes: [...(prev.notes || []), v] }));
|
}));
|
||||||
}
|
|
||||||
setCurrentNote('');
|
setCurrentNote('');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeNote = (note: string) => {
|
const removeNote = (note: string) => {
|
||||||
setFolderData(prev => ({ ...prev, notes: (prev.notes || []).filter(n => n !== note) }));
|
setFolderData(prev => ({
|
||||||
|
...prev,
|
||||||
|
notes: prev.notes.filter(m => m !== note)
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ---------- Submit / Cancel ---------- */
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (onSave) {
|
if (onSave) {
|
||||||
@ -128,32 +109,22 @@ function FolderModal({
|
|||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (onClose) {
|
|
||||||
onClose(); // ← ça ferme le modal après sauvegarde
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
if (onCancel) {
|
if (onCancel) {
|
||||||
onCancel(); // ton callback spécifique
|
onCancel();
|
||||||
} else if (onClose) {
|
} else {
|
||||||
onClose(); // fallback si pas de onCancel
|
onClose();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Title text
|
|
||||||
const title = `Créer un dossier ${capitalize(folderType)}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// On ne fait PAS "if (!isOpen) return null" : Modal gère l'animation/visibilité
|
<Modal isOpen={isOpen} onClose={onClose} title="Créer un nouveau dossier" size="lg">
|
||||||
<Modal isOpen={isOpen} onClose={onClose} title={title} size="lg">
|
<div className="folder-container">
|
||||||
<div className={`folder-container folder-${folderType}`}>
|
|
||||||
<form className="folder-form" onSubmit={handleSubmit}>
|
<form className="folder-form" onSubmit={handleSubmit}>
|
||||||
|
|
||||||
{/* Informations principales */}
|
|
||||||
<div className="form-section">
|
<div className="form-section">
|
||||||
<h3 className="section-title">Informations principales</h3>
|
<h3 className="section-title">Informations principales</h3>
|
||||||
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<div className="form-field">
|
<div className="form-field">
|
||||||
<label>
|
<label>
|
||||||
@ -162,14 +133,13 @@ function FolderModal({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="folderNumber"
|
name="folderNumber"
|
||||||
value={folderData.folderNumber || ''}
|
value={folderData.folderNumber}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
required
|
required
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
placeholder="ex: DOC-2025-001"
|
placeholder="ex: DOC-2025-001"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-field">
|
<div className="form-field">
|
||||||
<label>
|
<label>
|
||||||
Nom <span className="required">*</span>
|
Nom <span className="required">*</span>
|
||||||
@ -177,7 +147,7 @@ function FolderModal({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
name="name"
|
||||||
value={folderData.name || ''}
|
value={folderData.name}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
required
|
required
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
@ -186,11 +156,52 @@ function FolderModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="form-field">
|
||||||
|
<label>
|
||||||
|
Type d'acte <span className="required">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="deedType"
|
||||||
|
value={folderData.deedType}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
disabled={readOnly}
|
||||||
|
>
|
||||||
|
<option value="">Sélectionnez le type d'acte</option>
|
||||||
|
<option value="vente">Vente</option>
|
||||||
|
<option value="achat">Achat</option>
|
||||||
|
<option value="succession">Succession</option>
|
||||||
|
<option value="donation">Donation</option>
|
||||||
|
<option value="hypotheque">Hypothèque</option>
|
||||||
|
<option value="bail">Bail</option>
|
||||||
|
<option value="autre">Autre</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label>
|
||||||
|
Statut <span className="required">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="status"
|
||||||
|
value={folderData.status}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
disabled={readOnly}
|
||||||
|
>
|
||||||
|
<option value="active">Actif</option>
|
||||||
|
<option value="pending">En attente</option>
|
||||||
|
<option value="completed">Complété</option>
|
||||||
|
<option value="archived">Archivé</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-field">
|
<div className="form-field">
|
||||||
<label>Description</label>
|
<label>Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
name="description"
|
name="description"
|
||||||
value={folderData.description || ''}
|
value={folderData.description}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
placeholder="Description du dossier"
|
placeholder="Description du dossier"
|
||||||
@ -203,7 +214,7 @@ function FolderModal({
|
|||||||
<label>Description d'archivage</label>
|
<label>Description d'archivage</label>
|
||||||
<textarea
|
<textarea
|
||||||
name="archived_description"
|
name="archived_description"
|
||||||
value={folderData.archived_description || ''}
|
value={folderData.archived_description}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
placeholder="Raison d'archivage"
|
placeholder="Raison d'archivage"
|
||||||
@ -213,19 +224,100 @@ function FolderModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Champs spécifiques injectés */}
|
|
||||||
{renderExtraFields && (
|
|
||||||
<div className="form-section">
|
<div className="form-section">
|
||||||
{renderExtraFields(folderData, setFolderData)}
|
<h3 className="section-title">Clients</h3>
|
||||||
|
<div className="tag-list">
|
||||||
|
{folderData.customers.map((customer, index) => (
|
||||||
|
<div key={index} className="tag-item">
|
||||||
|
<span>{customer}</span>
|
||||||
|
{!readOnly && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="tag-remove"
|
||||||
|
onClick={() => removeCustomer(customer)}
|
||||||
|
aria-label="Supprimer ce client"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!readOnly && (
|
||||||
|
<div className="form-field tag-input-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={currentCustomer}
|
||||||
|
onChange={(e) => setCurrentCustomer(e.target.value)}
|
||||||
|
placeholder="Ajouter un client"
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
addCustomer();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-add-tag"
|
||||||
|
onClick={addCustomer}
|
||||||
|
>
|
||||||
|
Ajouter
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-section">
|
||||||
|
<h3 className="section-title">Parties prenantes</h3>
|
||||||
|
<div className="tag-list">
|
||||||
|
{folderData.stakeholders.map((stakeholder, index) => (
|
||||||
|
<div key={index} className="tag-item">
|
||||||
|
<span>{stakeholder}</span>
|
||||||
|
{!readOnly && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="tag-remove"
|
||||||
|
onClick={() => removeStakeholder(stakeholder)}
|
||||||
|
aria-label="Supprimer cette partie prenante"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!readOnly && (
|
||||||
|
<div className="form-field tag-input-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={currentStakeholder}
|
||||||
|
onChange={(e) => setCurrentStakeholder(e.target.value)}
|
||||||
|
placeholder="Ajouter une partie prenante"
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
addStakeholder();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-add-tag"
|
||||||
|
onClick={addStakeholder}
|
||||||
|
>
|
||||||
|
Ajouter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Notes */}
|
|
||||||
<div className="form-section">
|
<div className="form-section">
|
||||||
<h3 className="section-title">Notes</h3>
|
<h3 className="section-title">Notes</h3>
|
||||||
|
|
||||||
<div className="tag-list">
|
<div className="tag-list">
|
||||||
{(folderData.notes || []).map((note, index) => (
|
{folderData.notes.map((note, index) => (
|
||||||
<div key={index} className="tag-item">
|
<div key={index} className="tag-item">
|
||||||
<span>{note}</span>
|
<span>{note}</span>
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
@ -249,7 +341,7 @@ function FolderModal({
|
|||||||
value={currentNote}
|
value={currentNote}
|
||||||
onChange={(e) => setCurrentNote(e.target.value)}
|
onChange={(e) => setCurrentNote(e.target.value)}
|
||||||
placeholder="Ajouter une note"
|
placeholder="Ajouter une note"
|
||||||
onKeyDown={(e) => {
|
onKeyPress={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
addNote();
|
addNote();
|
||||||
@ -267,16 +359,14 @@ function FolderModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Informations système */}
|
|
||||||
<div className="form-section">
|
<div className="form-section">
|
||||||
<h3 className="section-title">Informations système</h3>
|
<h3 className="section-title">Informations système</h3>
|
||||||
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<div className="form-field">
|
<div className="form-field">
|
||||||
<label>Créé le</label>
|
<label>Créé le</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={new Date(folderData.created_at).toLocaleString('fr-FR', {
|
value={new Date(folderData.created_at).toLocaleDateString('fr-FR', {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@ -287,12 +377,11 @@ function FolderModal({
|
|||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-field">
|
<div className="form-field">
|
||||||
<label>Dernière mise à jour</label>
|
<label>Dernière mise à jour</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={new Date(folderData.updated_at).toLocaleString('fr-FR', {
|
value={new Date(folderData.updated_at).toLocaleDateString('fr-FR', {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@ -306,12 +395,19 @@ function FolderModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="form-actions">
|
<div className="form-actions">
|
||||||
<button type="button" className="btn-cancel" onClick={handleCancel}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-cancel"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
Annuler
|
Annuler
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" className="btn-submit" disabled={readOnly}>
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn-submit"
|
||||||
|
disabled={readOnly}
|
||||||
|
>
|
||||||
Enregistrer
|
Enregistrer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -319,7 +415,7 @@ function FolderModal({
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
FolderModal.displayName = 'FolderModal';
|
FolderModal.displayName = 'FolderModal';
|
||||||
export default memo(FolderModal);
|
export default memo(FolderModal);
|
||||||
|
|||||||
@ -1,325 +0,0 @@
|
|||||||
import { useState, memo } from 'react';
|
|
||||||
import { isFileBlob, type FileBlob } from '@/lib/4nk/models/Data';
|
|
||||||
import { iframeUrl } from "@/app/page";
|
|
||||||
import MessageBus from '@/lib/4nk/MessageBus';
|
|
||||||
|
|
||||||
interface BlockState {
|
|
||||||
commited_in: string;
|
|
||||||
state_id: string;
|
|
||||||
pcd_commitment: Record<string, string>;
|
|
||||||
public_data: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Block {
|
|
||||||
states: BlockState[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Processes {
|
|
||||||
[key: string]: Block;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProcessesViewerProps {
|
|
||||||
processes: Processes | null;
|
|
||||||
myProcesses: string[];
|
|
||||||
onProcessesUpdate?: (processes: Processes) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const compareStates = (
|
|
||||||
currentState: BlockState,
|
|
||||||
index: number,
|
|
||||||
previousState?: BlockState,
|
|
||||||
currentPrivateData?: Record<string, any>,
|
|
||||||
previousPrivateData?: Record<string, any>
|
|
||||||
) => {
|
|
||||||
const result: Record<string, {
|
|
||||||
value: any,
|
|
||||||
status: 'unchanged' | 'modified',
|
|
||||||
hash?: string,
|
|
||||||
isPrivate: boolean,
|
|
||||||
stateId: string
|
|
||||||
}> = {};
|
|
||||||
|
|
||||||
Object.keys(currentState.public_data).forEach(key => {
|
|
||||||
const currentValue = currentState.public_data[key];
|
|
||||||
const previousValue = previousState?.public_data[key];
|
|
||||||
const isModified = index > 0 && previousValue !== undefined && JSON.stringify(currentValue) !== JSON.stringify(previousValue);
|
|
||||||
|
|
||||||
result[key] = {
|
|
||||||
value: currentValue,
|
|
||||||
status: isModified ? 'modified' : 'unchanged',
|
|
||||||
hash: currentState.pcd_commitment[key],
|
|
||||||
isPrivate: false,
|
|
||||||
stateId: currentState.state_id
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (index === 0 && currentPrivateData) {
|
|
||||||
Object.entries(currentPrivateData).forEach(([key, value]) => {
|
|
||||||
result[key] = {
|
|
||||||
value,
|
|
||||||
status: 'unchanged',
|
|
||||||
hash: currentState.pcd_commitment[key],
|
|
||||||
isPrivate: true,
|
|
||||||
stateId: currentState.state_id
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (previousPrivateData) {
|
|
||||||
Object.entries(previousPrivateData).forEach(([key, value]) => {
|
|
||||||
result[key] = {
|
|
||||||
value,
|
|
||||||
status: 'unchanged',
|
|
||||||
hash: previousState?.pcd_commitment[key],
|
|
||||||
isPrivate: true,
|
|
||||||
stateId: previousState!.state_id
|
|
||||||
};
|
|
||||||
});
|
|
||||||
if (currentPrivateData) {
|
|
||||||
Object.entries(currentPrivateData).forEach(([key, value]) => {
|
|
||||||
result[key] = {
|
|
||||||
value,
|
|
||||||
status: 'modified',
|
|
||||||
hash: currentState.pcd_commitment[key],
|
|
||||||
isPrivate: true,
|
|
||||||
stateId: currentState.state_id
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
function ProcessesViewer({ processes, myProcesses, onProcessesUpdate }: ProcessesViewerProps) {
|
|
||||||
const [expandedBlocks, setExpandedBlocks] = useState<string[]>([]);
|
|
||||||
const [isFiltered, setIsFiltered] = useState<boolean>(false);
|
|
||||||
const [privateData, setPrivateData] = useState<Record<string, Record<string, any>>>({});
|
|
||||||
const [editingField, setEditingField] = useState<{processId: string; stateId: string; key: string; value: any;} | null>(null);
|
|
||||||
const [tempValue, setTempValue] = useState<any>(null);
|
|
||||||
|
|
||||||
const toggleBlock = (blockId: string) => {
|
|
||||||
setExpandedBlocks(prev => prev.includes(blockId) ? prev.filter(id => id !== blockId) : [...prev, blockId]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterClick = () => setIsFiltered(prev => !prev);
|
|
||||||
|
|
||||||
if (!processes || Object.keys(processes).length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
|
|
||||||
<h3 className="text-lg font-medium mb-2">Aucun processus disponible</h3>
|
|
||||||
<p>Connectez-vous pour voir vos processus</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchPrivateData = async (processId: string, stateId: string) => {
|
|
||||||
if (!expandedBlocks.includes(processId) || !myProcesses.includes(processId)) return;
|
|
||||||
try {
|
|
||||||
const messageBus = MessageBus.getInstance(iframeUrl);
|
|
||||||
await messageBus.isReady();
|
|
||||||
const data = await messageBus.getData(processId, stateId);
|
|
||||||
setPrivateData(prev => ({ ...prev, [stateId]: data }));
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownload = (name: string | undefined, fileBlob: FileBlob) => {
|
|
||||||
const blob = new Blob([fileBlob.data], { type: fileBlob.type });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = name || 'download';
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatValue = (key: string, value: string | number[] | FileBlob) => {
|
|
||||||
if (isFileBlob(value)) {
|
|
||||||
return (
|
|
||||||
<button className="text-blue-600 hover:underline" onClick={() => handleDownload(key, value)}>
|
|
||||||
📥 Télécharger
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <span>{JSON.stringify(value || '')}</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDataIcon = (value: any) => {
|
|
||||||
if (isFileBlob(value)) return '📄';
|
|
||||||
if (typeof value === 'string') return '📝';
|
|
||||||
if (typeof value === 'number') return '🔢';
|
|
||||||
if (Array.isArray(value)) return '📋';
|
|
||||||
if (typeof value === 'boolean') return '✅';
|
|
||||||
return '📦';
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFieldUpdate = async (processId: string, stateId: string, key: string, value: any) => {
|
|
||||||
try {
|
|
||||||
const messageBus = MessageBus.getInstance(iframeUrl);
|
|
||||||
await messageBus.isReady();
|
|
||||||
const updatedProcess = await messageBus.updateProcess(processId, stateId, { [key]: value }, [], null);
|
|
||||||
if (!updatedProcess) throw new Error('No updated process found');
|
|
||||||
const newStateId = updatedProcess.diffs[0]?.state_id;
|
|
||||||
if (!newStateId) throw new Error('No new state id found');
|
|
||||||
await messageBus.notifyProcessUpdate(processId, newStateId);
|
|
||||||
await messageBus.validateState(processId, newStateId);
|
|
||||||
const updatedProcesses = await messageBus.getProcesses();
|
|
||||||
onProcessesUpdate?.(updatedProcesses);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderEditForm = (key: string, value: any, onSave: (v: any) => void, onCancel: () => void) => {
|
|
||||||
if (tempValue === null) setTempValue(value);
|
|
||||||
|
|
||||||
const handleFormClick = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); };
|
|
||||||
|
|
||||||
if (isFileBlob(value)) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col space-y-2" onClick={handleFormClick}>
|
|
||||||
<input type="file" onChange={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (file) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (event) => {
|
|
||||||
if (event.target?.result) {
|
|
||||||
setTempValue({ type: file.type, data: new Uint8Array(event.target.result as ArrayBuffer) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsArrayBuffer(file);
|
|
||||||
}
|
|
||||||
}}/>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button className="px-2 py-1 bg-blue-500 text-white rounded" onClick={() => { onSave(tempValue); setTempValue(null); }}>Sauvegarder</button>
|
|
||||||
<button className="px-2 py-1 bg-gray-300 rounded" onClick={() => { onCancel(); setTempValue(null); }}>Annuler</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'boolean') {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center space-x-2" onClick={handleFormClick}>
|
|
||||||
<select className="border rounded px-2 py-1" value={tempValue.toString()} onChange={(e) => setTempValue(e.target.value === 'true')}>
|
|
||||||
<option value="true">Vrai</option>
|
|
||||||
<option value="false">Faux</option>
|
|
||||||
</select>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button className="px-2 py-1 bg-blue-500 text-white rounded" onClick={() => onSave(tempValue)}>Sauvegarder</button>
|
|
||||||
<button className="px-2 py-1 bg-gray-300 rounded" onClick={() => onCancel()}>Annuler</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col space-y-2" onClick={handleFormClick}>
|
|
||||||
<textarea className="border rounded p-2" rows={4} value={JSON.stringify(tempValue, null, 2)} onChange={(e) => {
|
|
||||||
try { const parsed = JSON.parse(e.target.value); if (Array.isArray(parsed)) setTempValue(parsed); } catch {}
|
|
||||||
}}/>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button className="px-2 py-1 bg-blue-500 text-white rounded" onClick={() => onSave(tempValue)}>Sauvegarder</button>
|
|
||||||
<button className="px-2 py-1 bg-gray-300 rounded" onClick={() => onCancel()}>Annuler</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex space-x-2 items-center" onClick={handleFormClick}>
|
|
||||||
<input className="border rounded px-2 py-1" type={typeof value === 'number' ? 'number' : 'text'} value={tempValue} onChange={(e) => setTempValue(typeof value === 'number' ? parseFloat(e.target.value) : e.target.value)} autoFocus />
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button className="px-2 py-1 bg-blue-500 text-white rounded" onClick={() => onSave(tempValue)}>Sauvegarder</button>
|
|
||||||
<button className="px-2 py-1 bg-gray-300 rounded" onClick={() => onCancel()}>Annuler</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderDataField = (key: string, value: any, hash: string | undefined, isPrivate: boolean, processId: string, stateId: string, status: 'unchanged' | 'modified' = 'unchanged', originStateId?: string) => {
|
|
||||||
const isEditing = editingField?.key === key && editingField?.processId === processId && editingField?.stateId === stateId;
|
|
||||||
return (
|
|
||||||
<div key={key} className={`border rounded p-2 mb-2 transition-colors ${status === 'modified' ? 'bg-green-100' : 'bg-white'}`}>
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<span title={isPrivate ? 'Donnée privée' : 'Donnée publique'}>{isPrivate ? '🔒' : '🌐'}</span>
|
|
||||||
<span>{getDataIcon(value)}</span>
|
|
||||||
<span className="font-medium">{key}</span>
|
|
||||||
{originStateId && originStateId !== stateId && <span title={`Propagé depuis l'état ${originStateId}`}>↺</span>}
|
|
||||||
</div>
|
|
||||||
<button className="text-sm text-gray-500 hover:text-gray-700" onClick={(e) => { e.stopPropagation(); setEditingField({ processId, stateId, key, value }); }}>
|
|
||||||
{isEditing ? '✕' : '🔄'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{isEditing ? renderEditForm(key, value, async (v) => { await handleFieldUpdate(processId, stateId, key, v); setEditingField(null); setTempValue(null); }, () => { setEditingField(null); setTempValue(null); }) : (
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<span>{formatValue(key, value)}</span>
|
|
||||||
{hash && <span title={`Hash: ${hash}`}>🔑</span>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full overflow-auto p-2">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h2 className="text-xl font-semibold">Processus</h2>
|
|
||||||
<button className="px-2 py-1 border rounded text-sm" onClick={handleFilterClick}>{isFiltered ? 'Show All' : 'Filter'}</button>
|
|
||||||
</div>
|
|
||||||
<p className="mb-2 text-sm text-gray-500">{isFiltered ? Object.keys(processes).filter(p => myProcesses.includes(p)).length : Object.keys(processes).length} processus disponible(s)</p>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{Object.entries(processes).map(([processId, process]) => {
|
|
||||||
if (isFiltered && !myProcesses.includes(processId)) return null;
|
|
||||||
const isExpanded = expandedBlocks.includes(processId);
|
|
||||||
const stateCount = process.states.length - 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={processId} className="border rounded shadow-sm">
|
|
||||||
<div className="flex justify-between items-center p-2 cursor-pointer bg-gray-50" onClick={() => toggleBlock(processId)}>
|
|
||||||
<div className="font-mono">{processId.slice(0,8)}...{processId.slice(-4)}</div>
|
|
||||||
<div>{stateCount} état(s)</div>
|
|
||||||
<div>{isExpanded ? '▼' : '▶'}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isExpanded && (
|
|
||||||
<div className="p-2 space-y-2 bg-white">
|
|
||||||
<div><strong>Process ID:</strong> {processId}</div>
|
|
||||||
{process.states.map((state, index) => {
|
|
||||||
if (index === stateCount) return null;
|
|
||||||
if (myProcesses.includes(processId) && !privateData[state.state_id]) setTimeout(() => fetchPrivateData(processId, state.state_id), 0);
|
|
||||||
const statePrivateData = privateData[state.state_id] || {};
|
|
||||||
const stateData = compareStates(state, index, index > 0 ? process.states[index-1] : undefined, statePrivateData, index > 0 ? privateData[process.states[index-1].state_id] : undefined);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={`${processId}-state-${index}`} className="border-t pt-2">
|
|
||||||
<h4 className="font-medium mb-1">État {index+1}</h4>
|
|
||||||
<div className="text-sm mb-1"><strong>TransactionId:</strong> {state.commited_in}</div>
|
|
||||||
<div className="text-sm mb-2"><strong>Empreinte totale de l'état:</strong> {state.state_id}</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{Object.entries(stateData).map(([key, { value, status, hash, isPrivate, stateId }]) => renderDataField(key, value, hash, isPrivate, processId, stateId, status, state.state_id))}
|
|
||||||
{myProcesses.includes(processId) && Object.keys(statePrivateData).length === 0 && <div className="text-gray-400 text-sm">Chargement des données privées...</div>}
|
|
||||||
{!myProcesses.includes(processId) && <div className="text-gray-400 text-sm">🔒 Vous n'avez pas accès aux données privées de ce processus</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ProcessesViewer.displayName = 'ProcessesViewer';
|
|
||||||
export default memo(ProcessesViewer);
|
|
||||||
163
components/modal/Modal.css
Normal file
163
components/modal/Modal.css
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: rgba(35, 36, 42, 0.82);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
animation: modal-fadein 0.33s cubic-bezier(.4, 0, .2, 1);
|
||||||
|
backdrop-filter: blur(3.5px);
|
||||||
|
-webkit-backdrop-filter: blur(3.5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes modal-fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
background: #23242a;
|
||||||
|
border-radius: 18px;
|
||||||
|
min-width: 340px;
|
||||||
|
max-width: 95vw;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 0 24px 0;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 12px 48px 0 rgba(0, 0, 0, 0.34), 0 2px 12px 0 rgba(30, 34, 44, 0.10);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: modal-popin 0.34s cubic-bezier(.4, 0, .2, 1);
|
||||||
|
transition: box-shadow 0.2s, opacity 0.25s cubic-bezier(.4, 0, .2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container.modal-closing {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(32px) scale(0.97);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes modal-popin {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(32px) scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background: linear-gradient(90deg, #23242a 85%, #23242aEE 100%);
|
||||||
|
color: #fff;
|
||||||
|
padding: 22px 30px 14px 30px;
|
||||||
|
border-radius: 18px 18px 0 0;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(30, 34, 44, 0.06);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #e3e4e8;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
min-width: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.18s, color 0.18s;
|
||||||
|
z-index: 2;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close svg {
|
||||||
|
display: block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover,
|
||||||
|
.modal-close:focus {
|
||||||
|
background: rgba(255, 255, 255, 0.10);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:active {
|
||||||
|
background: rgba(67, 160, 71, 0.13);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:active {
|
||||||
|
background: rgba(67, 160, 71, 0.13);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 28px 28px 0 28px;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
color: #e3e4e8;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.modal-container {
|
||||||
|
min-width: 0;
|
||||||
|
width: 98vw;
|
||||||
|
padding: 0 0 12px 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 16px 10px 10px 14px;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 14px 8px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
38
components/modal/Modal.tsx
Normal file
38
components/modal/Modal.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import './Modal.css';
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
title?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Modal({ isOpen, onClose, title, children }: ModalProps) {
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay modal-fadein">
|
||||||
|
<div className="modal-container modal-popin">
|
||||||
|
<button className="close-button modal-close" onClick={onClose} aria-label="Fermer">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<path d="M6 6L18 18M18 6L6 18" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" filter="url(#shadow)" />
|
||||||
|
<defs>
|
||||||
|
<filter id="shadow" x="-2" y="-2" width="28" height="28" filterUnits="userSpaceOnUse">
|
||||||
|
<feDropShadow dx="0" dy="0" stdDeviation="1.2" floodColor="#23242a" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{title && <div className="modal-header modal-header"><h2>{title}</h2></div>}
|
||||||
|
<div className="modal-body modal-body">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.displayName = 'Modal';
|
||||||
|
export default memo(Modal);
|
||||||
@ -1,101 +1,104 @@
|
|||||||
/* Overlay */
|
/* Modal Overlay */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
top: 0;
|
||||||
background-color: rgba(17, 24, 39, 0.55);
|
left: 0;
|
||||||
backdrop-filter: blur(6px);
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-in { opacity: 1; }
|
/* Modal Container */
|
||||||
.fade-out { opacity: 0; }
|
|
||||||
|
|
||||||
/* Container */
|
|
||||||
.modal-container {
|
.modal-container {
|
||||||
background: #fff;
|
background: white;
|
||||||
border-radius: 1rem;
|
border-radius: 0.75rem;
|
||||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25);
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-sm { max-width: 28rem; }
|
.modal-sm {
|
||||||
.modal-md { max-width: 32rem; }
|
max-width: 28rem;
|
||||||
.modal-lg { max-width: 48rem; }
|
|
||||||
.modal-xl { max-width: 64rem; }
|
|
||||||
|
|
||||||
.slide-in {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-out {
|
.modal-md {
|
||||||
transform: translateY(20px);
|
max-width: 32rem;
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header */
|
.modal-lg {
|
||||||
|
max-width: 48rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-xl {
|
||||||
|
max-width: 64rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Header */
|
||||||
.modal-header {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 1.25rem 1.5rem;
|
padding: 1.5rem;
|
||||||
background: linear-gradient(90deg, #f9fafb, #f3f4f6);
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
background: #f9fafb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
font-size: 1.3rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #111827;
|
color: #111827;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Close Button */
|
|
||||||
.modal-close-button {
|
.modal-close-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 2.25rem;
|
width: 2rem;
|
||||||
height: 2.25rem;
|
height: 2rem;
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.375rem;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close-button:hover {
|
.modal-close-button:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
background-color: #f3f4f6;
|
||||||
color: #111827;
|
color: #374151;
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Content */
|
/* Modal Content */
|
||||||
.modal-content {
|
.modal-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 1.5rem;
|
padding: 0;
|
||||||
background: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeInSuccess {
|
/* Responsive */
|
||||||
from {
|
@media (max-width: 640px) {
|
||||||
opacity: 0;
|
.modal-overlay {
|
||||||
transform: scale(0.95);
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
to {
|
|
||||||
opacity: 1;
|
.modal-container {
|
||||||
transform: scale(1);
|
max-height: 95vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import './Modal.css';
|
import './Modal.css';
|
||||||
|
|
||||||
@ -17,19 +17,7 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
children,
|
children,
|
||||||
size = 'md'
|
size = 'md'
|
||||||
}) => {
|
}) => {
|
||||||
const [isVisible, setIsVisible] = useState(isOpen);
|
if (!isOpen) return null;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
setIsVisible(true);
|
|
||||||
} else {
|
|
||||||
// attendre que l'animation de fermeture se joue
|
|
||||||
const timer = setTimeout(() => setIsVisible(false), 300); // durée en ms = durée CSS
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
if (!isVisible) return null;
|
|
||||||
|
|
||||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
@ -38,8 +26,8 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`modal-overlay ${isOpen ? 'fade-in' : 'fade-out'}`} onClick={handleBackdropClick}>
|
<div className="modal-overlay" onClick={handleBackdropClick}>
|
||||||
<div className={`modal-container modal-${size} ${isOpen ? 'slide-in' : 'slide-out'}`}>
|
<div className={`modal-container modal-${size}`}>
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<h2 className="modal-title">{title}</h2>
|
<h2 className="modal-title">{title}</h2>
|
||||||
<button
|
<button
|
||||||
@ -50,7 +38,9 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
<X className="h-5 w-5" />
|
<X className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-content">{children}</div>
|
<div className="modal-content">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -199,10 +199,11 @@ export default class MessageBus {
|
|||||||
const accessToken = userStore.getAccessToken()!;
|
const accessToken = userStore.getAccessToken()!;
|
||||||
|
|
||||||
const correlationId = uuidv4();
|
const correlationId = uuidv4();
|
||||||
|
console.log(correlationId);
|
||||||
this.initMessageListener(correlationId);
|
this.initMessageListener(correlationId);
|
||||||
|
|
||||||
const unsubscribe = EventBus.getInstance().on('PROCESSES_RETRIEVED', (responseId: string, processes: any) => {
|
const unsubscribe = EventBus.getInstance().on('PROCESSES_RETRIEVED', (responseId: string, processes: any) => {
|
||||||
console.log('MessageBus - PROCESSES_RETRIEVED', processes);
|
console.log(responseId);
|
||||||
if (responseId !== correlationId) {
|
if (responseId !== correlationId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -451,7 +452,7 @@ export default class MessageBus {
|
|||||||
this.initMessageListener(correlationId);
|
this.initMessageListener(correlationId);
|
||||||
|
|
||||||
const unsubscribe = EventBus.getInstance().on('PROCESS_UPDATED', (responseId: string, updatedProcess: any) => {
|
const unsubscribe = EventBus.getInstance().on('PROCESS_UPDATED', (responseId: string, updatedProcess: any) => {
|
||||||
console.log('MessageBus - PROCESS_UPDATED', updatedProcess);
|
console.log('PROCESS_UPDATED', updatedProcess);
|
||||||
if (responseId !== correlationId) {
|
if (responseId !== correlationId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -529,7 +530,7 @@ export default class MessageBus {
|
|||||||
this.initMessageListener(correlationId);
|
this.initMessageListener(correlationId);
|
||||||
|
|
||||||
const unsubscribe = EventBus.getInstance().on('STATE_VALIDATED', (responseId: string, updatedProcess: any) => {
|
const unsubscribe = EventBus.getInstance().on('STATE_VALIDATED', (responseId: string, updatedProcess: any) => {
|
||||||
console.log('MessageBus - STATE_VALIDATED', updatedProcess);
|
console.log(updatedProcess);
|
||||||
if (responseId !== correlationId) {
|
if (responseId !== correlationId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -726,6 +727,7 @@ export default class MessageBus {
|
|||||||
EventBus.getInstance().emit('ERROR_PROCESS_UPDATED', correlationId, error);
|
EventBus.getInstance().emit('ERROR_PROCESS_UPDATED', correlationId, error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log('PROCESS_UPDATED', message);
|
||||||
EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
|
EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
|
||||||
EventBus.getInstance().emit('PROCESS_UPDATED', correlationId, message.updatedProcess);
|
EventBus.getInstance().emit('PROCESS_UPDATED', correlationId, message.updatedProcess);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -1,135 +0,0 @@
|
|||||||
import { isFileBlob, type FileBlob } from "./Data";
|
|
||||||
import type { RoleDefinition } from "./Roles";
|
|
||||||
|
|
||||||
export interface RhData {
|
|
||||||
folderNumber: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
nom_salarie: string;
|
|
||||||
poste: string;
|
|
||||||
num_secu: string;
|
|
||||||
message_private: string;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
types_documents: string[];
|
|
||||||
rhs: string[];
|
|
||||||
salaries: string[];
|
|
||||||
notes: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isRhData(data: any): data is RhData {
|
|
||||||
if (typeof data !== 'object' || data === null) return false;
|
|
||||||
|
|
||||||
const requiredStringFields = [
|
|
||||||
'folderNumber',
|
|
||||||
'name',
|
|
||||||
'description',
|
|
||||||
'nom_salarie',
|
|
||||||
'poste',
|
|
||||||
'num_secu',
|
|
||||||
'message_private',
|
|
||||||
'created_at',
|
|
||||||
'updated_at'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const field of requiredStringFields) {
|
|
||||||
if (typeof data[field] !== 'string') return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiredArrayFields = [
|
|
||||||
'types_documents',
|
|
||||||
'rhs',
|
|
||||||
'salaries',
|
|
||||||
'notes',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const field of requiredArrayFields) {
|
|
||||||
if (!Array.isArray(data[field]) || !data[field].every((item: any) => typeof item === 'string')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyRhData: RhData = {
|
|
||||||
folderNumber: '',
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
nom_salarie: '',
|
|
||||||
poste: '',
|
|
||||||
num_secu: '',
|
|
||||||
message_private: '',
|
|
||||||
created_at: '',
|
|
||||||
updated_at: '',
|
|
||||||
types_documents: [],
|
|
||||||
rhs: [],
|
|
||||||
salaries: [],
|
|
||||||
notes: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const rhDataFields: string[] = Object.keys(emptyRhData);
|
|
||||||
|
|
||||||
const RhPublicFields: string[] = [
|
|
||||||
'nom_salarie',
|
|
||||||
'poste'
|
|
||||||
];
|
|
||||||
|
|
||||||
// All the attributes are private in that case
|
|
||||||
export const RhPrivateFields = [
|
|
||||||
...rhDataFields.filter(key => !RhPublicFields.includes(key))
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface RhCreated {
|
|
||||||
processId: string,
|
|
||||||
process: any, // Process
|
|
||||||
rhData: RhData,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setDefaultRhRoles(ownerId: string, rhs: string[], salaries: string[]): Record<string, RoleDefinition> {
|
|
||||||
return {
|
|
||||||
demiurge: {
|
|
||||||
members: [ownerId],
|
|
||||||
validation_rules: [],
|
|
||||||
storages: []
|
|
||||||
},
|
|
||||||
owner: {
|
|
||||||
members: [ownerId],
|
|
||||||
validation_rules: [
|
|
||||||
{
|
|
||||||
quorum: 0.5,
|
|
||||||
fields: [...rhDataFields, 'roles'],
|
|
||||||
min_sig_member: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
storages: []
|
|
||||||
},
|
|
||||||
rhs: {
|
|
||||||
members: rhs,
|
|
||||||
validation_rules: [
|
|
||||||
{
|
|
||||||
quorum: 0.5,
|
|
||||||
fields: rhDataFields,
|
|
||||||
min_sig_member: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
storages: []
|
|
||||||
},
|
|
||||||
salaries: {
|
|
||||||
members: salaries,
|
|
||||||
validation_rules: [
|
|
||||||
{
|
|
||||||
quorum: 0.0,
|
|
||||||
fields: rhDataFields,
|
|
||||||
min_sig_member: 0.0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
storages: []
|
|
||||||
},
|
|
||||||
apophis: {
|
|
||||||
members: [ownerId],
|
|
||||||
validation_rules: [],
|
|
||||||
storages: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Loading…
x
Reference in New Issue
Block a user