Compare commits

..

10 Commits

13 changed files with 758 additions and 3752 deletions

View File

@ -21,9 +21,10 @@ import {
LogOut, LogOut,
ChevronDown, ChevronDown,
Home, Home,
Key,
TestTube, TestTube,
User User,
Copy,
CheckCircle,
} from "@/lib/icons" } from "@/lib/icons"
import UserStore from "@/lib/4nk/UserStore" import UserStore from "@/lib/4nk/UserStore"
import EventBus from "@/lib/4nk/EventBus" import EventBus from "@/lib/4nk/EventBus"
@ -39,12 +40,11 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false) const [isAuthModalOpen, setIsAuthModalOpen] = useState(false)
const [show4nkAuthModal, setShow4nkAuthModal] = useState(false) const [show4nkAuthModal, setShow4nkAuthModal] = useState(false)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [isMockMode, setIsMockMode] = useState(false) const [isMockMode, setIsMockMode] = useState(true)
const [userInfo, setUserInfo] = useState<any>(null) const [userInfo, setUserInfo] = useState<any>(null)
const [showLogoutConfirm, setShowLogoutConfirm] = useState(false) const [showLogoutConfirm, setShowLogoutConfirm] = useState(false)
const [isPrivateKeyFlash, setIsPrivateKeyFlash] = useState(false) const [isCopied, setIsCopied] = useState(false)
const router = useRouter() const router = useRouter()
const pathname = usePathname()
useEffect(() => { useEffect(() => {
try { try {
@ -114,20 +114,16 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
setShowLogoutConfirm(true) setShowLogoutConfirm(true)
}, []); }, []);
useEffect(() => { const handleCopyToClipboard = useCallback(() => {
const onPrivateKeyAccess = () => { if (userPairingId) {
setIsPrivateKeyFlash(true) navigator.clipboard.writeText(userPairingId).then(() => {
setTimeout(() => setIsPrivateKeyFlash(false), 400) setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
}).catch(err => {
console.error('Erreur lors de la copie : ', err);
});
} }
if (typeof window !== "undefined") { }, [userPairingId]);
window.addEventListener("private-key-access", onPrivateKeyAccess as EventListener)
}
return () => {
if (typeof window !== "undefined") {
window.removeEventListener("private-key-access", onPrivateKeyAccess as EventListener)
}
}
}, [])
return ( return (
<div className="flex h-screen bg-gray-50 dark:bg-gray-900"> <div className="flex h-screen bg-gray-50 dark:bg-gray-900">
@ -180,6 +176,27 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56"> <DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel> <DropdownMenuLabel>
<div className="flex items-center justify-between group">
<p className="text-sm font-medium truncate" title={userPairingId || ""}>
{userInfo?.id}
</p>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleCopyToClipboard();
}}
>
{isCopied ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4 text-gray-500 group-hover:text-gray-900 dark:group-hover:text-gray-100" />
)}
</Button>
</div>
<p className="text-sm font-medium">{userInfo?.name}</p> <p className="text-sm font-medium">{userInfo?.name}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{userInfo?.company}</p> <p className="text-xs text-gray-500 dark:text-gray-400">{userInfo?.company}</p>
</DropdownMenuLabel> </DropdownMenuLabel>

View File

@ -96,7 +96,6 @@ export default function DashboardPage() {
const [folderType, setFolderType] = useState<FolderType | null>(null); const [folderType, setFolderType] = useState<FolderType | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [notification, setNotification] = useState<{ type: "success" | "error" | "info"; message: string } | null>(null) const [notification, setNotification] = useState<{ type: "success" | "error" | "info"; message: string } | null>(null)
const [selectedFolder, setSelectedFolder] = useState<EnrichedFolderData | null>(null); const [selectedFolder, setSelectedFolder] = useState<EnrichedFolderData | null>(null);
const { const {
@ -104,6 +103,7 @@ export default function DashboardPage() {
userPairingId, userPairingId,
folders, folders,
loadingFolders, loadingFolders,
members,
setFolderProcesses, setFolderProcesses,
setMyFolderProcesses, setMyFolderProcesses,
setFolderPrivateData setFolderPrivateData
@ -131,33 +131,46 @@ export default function DashboardPage() {
}; };
const handleSaveNewFolder = useCallback( const handleSaveNewFolder = useCallback(
(folderData: FolderData) => { (folderData: FolderData, selectedMembers: string[]) => {
if (!isConnected || !userPairingId) { if (!isConnected || !userPairingId) {
showNotification("error", "Vous devez être connecté à 4NK pour créer un dossier"); showNotification("error", "Vous devez être connecté à 4NK pour créer un dossier");
return; return;
} }
// Crée les rôles par défaut (probablement 'owner' = vous)
const roles = setDefaultFolderRoles(userPairingId); const roles = setDefaultFolderRoles(userPairingId);
const folderPrivateFields = FolderPrivateFields; const folderPrivateFields = FolderPrivateFields;
MessageBus.getInstance(iframeUrl)
.createFolder(folderData, folderPrivateFields, roles)
.then((_folderCreated: FolderCreated) => {
const firstStateId = _folderCreated.process.states[0].state_id;
MessageBus.getInstance(iframeUrl)
.notifyProcessUpdate(_folderCreated.processId, firstStateId)
.then(() => {
const { processId, process } = _folderCreated;
setFolderProcesses((prevProcesses: any) => ({ ...prevProcesses, [processId]: process })); // Fusionne votre userPairingId avec les membres sélectionnés
setMyFolderProcesses((prevMyProcesses: string[]) => { // On utilise un Set pour éviter les doublons
if (prevMyProcesses.includes(processId)) return prevMyProcesses; const allOwnerMembers = new Set([
return [...prevMyProcesses, processId]; ...roles.owner.members, // Membres par défaut (vous)
}); userPairingId, // S'assurer que vous y êtes
setFolderPrivateData((prevData) => ({ ...prevData, [firstStateId]: folderData })); ...selectedMembers // Ajoute les nouveaux membres
]);
showNotification("success", "Dossier créé avec succès !"); // Met à jour la liste des membres pour le rôle 'owner'
handleCloseModal(); // (Vous pouvez ajuster "owner" pour un autre rôle si nécessaire)
roles.owner.members = Array.from(allOwnerMembers);
console.log(roles);
MessageBus.getInstance(iframeUrl).createFolder(folderData, folderPrivateFields, roles).then((_folderCreated: FolderCreated) => {
MessageBus.getInstance(iframeUrl).notifyProcessUpdate(_folderCreated.processId, _folderCreated.process.states[0].state_id).then(() => {
MessageBus.getInstance(iframeUrl).validateState(_folderCreated.processId, _folderCreated.process.states[0].state_id).then((_updatedProcess: any) => {
const { processId, process } = _folderCreated;
setFolderProcesses((prevProcesses: any) => ({ ...prevProcesses, [processId]: process }));
setMyFolderProcesses((prevMyProcesses: string[]) => {
if (prevMyProcesses.includes(processId)) return prevMyProcesses;
return [...prevMyProcesses, processId];
}); });
}) setFolderPrivateData((prevData) => ({ ...prevData, [_folderCreated.process.states[0].state_id]: folderData }));
showNotification("success", "Dossier créé avec succès !");
handleCloseModal();
});
});
})
.catch((error: any) => { .catch((error: any) => {
console.error('Erreur lors de la création du dossier:', error); console.error('Erreur lors de la création du dossier:', error);
showNotification("error", "Erreur lors de la création du dossier"); showNotification("error", "Erreur lors de la création du dossier");
@ -222,7 +235,7 @@ export default function DashboardPage() {
<Folder className="h-5 w-5 text-blue-500 flex-shrink-0" /> <Folder className="h-5 w-5 text-blue-500 flex-shrink-0" />
<div className="min-w-0"> <div className="min-w-0">
<h3 className="font-medium text-gray-100 truncate">{folder.name}</h3> <h3 className="font-medium text-gray-100 truncate">{folder.name}</h3>
{/* ID du dossier supprimé */} {/* Texte sous le nom du dossier */}
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -245,7 +258,24 @@ export default function DashboardPage() {
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<h1 className="text-2xl font-semibold">{selectedFolder.name}</h1> <h1 className="text-2xl font-semibold">{selectedFolder.name}</h1>
<p className="text-gray-400 mt-2">{selectedFolder.description}</p> <p className="text-gray-400 mt-2">{selectedFolder.description}</p>
{/* Badge ID supprimé */} <div className="flex items-center space-x-4 mt-3 text-xs text-gray-500 dark:text-gray-400">
<div className="flex items-center space-x-1" title={selectedFolder.created_at}>
<Clock className="h-3 w-3" />
<span>
Créé le: {new Date(selectedFolder.created_at).toLocaleString('fr-FR', {
day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'
})}
</span>
</div>
<div className="flex items-center space-x-1" title={selectedFolder.updated_at}>
<Clock className="h-3 w-3" />
<span>
Modifié le: {new Date(selectedFolder.updated_at).toLocaleString('fr-FR', {
day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'
})}
</span>
</div>
</div>
</div> </div>
{/* Contenu Colonne 2 */} {/* Contenu Colonne 2 */}
@ -279,7 +309,7 @@ export default function DashboardPage() {
Fichiers Fichiers
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> {/* <CardContent>
{selectedFolder.files && selectedFolder.files.length > 0 ? ( {selectedFolder.files && selectedFolder.files.length > 0 ? (
<div className="space-y-3"> <div className="space-y-3">
{selectedFolder.files.map((file, index) => ( {selectedFolder.files.map((file, index) => (
@ -297,7 +327,7 @@ export default function DashboardPage() {
) : ( ) : (
<p className="text-gray-500">Aucun fichier dans ce dossier.</p> <p className="text-gray-500">Aucun fichier dans ce dossier.</p>
)} )}
</CardContent> </CardContent> */}
</Card> </Card>
</div> </div>
</> </>
@ -319,6 +349,7 @@ export default function DashboardPage() {
onSave={handleSaveNewFolder} onSave={handleSaveNewFolder}
onCancel={handleCloseModal} onCancel={handleCloseModal}
folderType={folderType || "autre"} folderType={folderType || "autre"}
members={members}
/> />
)} )}
{notification && ( {notification && (

View File

@ -13,6 +13,8 @@ import {
MessageSquare MessageSquare
} from "lucide-react" } from "lucide-react"
import type { EnrichedFolderData } from "@/lib/contexts/FourNKContext"; import type { EnrichedFolderData } from "@/lib/contexts/FourNKContext";
import MessageBus from "@/lib/4nk/MessageBus"
import { iframeUrl } from "@/app/page"
// Interface pour les props (accepte null) // Interface pour les props (accepte null)
interface FolderChatProps { interface FolderChatProps {
@ -45,10 +47,50 @@ export default function FolderChat({ folder }: FolderChatProps) {
// Filtre les messages basé sur l'onglet actif // Filtre les messages basé sur l'onglet actif
const filteredMessages = mockMessages.filter(msg => msg.type === activeTab); const filteredMessages = mockMessages.filter(msg => msg.type === activeTab);
const handleProcessUpdate = async (processId: string, key: string, value: any) => {
try {
const messageBus = MessageBus.getInstance(iframeUrl);
await messageBus.isReady();
const updateData = {
[key]: value
};
// First update the process
const updatedProcess = await messageBus.updateProcess(processId, updateData, [], 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');
}
// Then notify about the update
await messageBus.notifyProcessUpdate(processId, newStateId);
// Finally validate the state
await messageBus.validateState(processId, newStateId);
// Refresh the processes data
// const updatedProcesses = await messageBus.getProcesses();
console.log('Process updated successfully');
} catch (error) {
console.error('Error updating field:', error);
// You might want to show an error message to the user here
}
};
const handleSendMessage = () => { const handleSendMessage = () => {
if (newMessage.trim()) { if (newMessage.trim()) {
console.log(`Envoi message [${activeTab}] à:`, folder?.folderNumber, "Msg:", newMessage) console.log(`Envoi message [${activeTab}] à:`, folder?.folderNumber, "Msg:", newMessage)
// TODO: Implémenter la logique d'envoi de message // TODO: Implémenter la logique d'envoi de message
if(!folder) return;
const key = activeTab === 'owner' ? 'messages_owner' : 'messages'
handleProcessUpdate(folder.processId, key, newMessage)
setNewMessage("") setNewMessage("")
} }
} }
@ -131,8 +173,8 @@ export default function FolderChat({ folder }: FolderChatProps) {
<div> <div>
<div <div
className={`p-3 rounded-lg ${msg.sender === 'me' className={`p-3 rounded-lg ${msg.sender === 'me'
? 'bg-blue-600 text-white rounded-br-none' ? 'bg-blue-600 text-white rounded-br-none'
: 'bg-gray-700 text-gray-100 rounded-bl-none' : 'bg-gray-700 text-gray-100 rounded-bl-none'
}`} }`}
> >
{msg.sender === 'other' && ( {msg.sender === 'other' && (

View File

@ -1,17 +1,21 @@
import React, { useEffect, useState, memo } from 'react'; import React, { useEffect, useState, memo } from 'react';
import Modal from './Modal'; import Modal from './Modal';
import type { FolderData } from '@/lib/4nk/models/FolderData'; import type { FolderData } from '@/lib/4nk/models/FolderData';
import { MemberAutocomplete } from '../ui/member-autocomplete';
type FolderType = 'contrat' | 'projet' | 'rapport' | 'finance' | 'rh' | 'marketing' | 'autre'; type FolderType = 'contrat' | 'projet' | 'rapport' | 'finance' | 'rh' | 'marketing' | 'autre';
interface FolderModalProps { interface FolderModalProps {
folder?: FolderData; folder?: FolderData;
onSave?: (folderData: FolderData) => void; // --- MODIFIÉ ---
onSave?: (folderData: FolderData, selectedMembers: string[]) => void;
onCancel?: () => void; onCancel?: () => void;
readOnly?: boolean; readOnly?: boolean;
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
folderType?: FolderType; folderType?: FolderType;
// --- NOUVEAU ---
members?: string[]; // Liste des membres disponibles
renderExtraFields?: ( renderExtraFields?: (
folderData: FolderData, folderData: FolderData,
setFolderData: React.Dispatch<React.SetStateAction<FolderData>> setFolderData: React.Dispatch<React.SetStateAction<FolderData>>
@ -24,7 +28,9 @@ const defaultFolder: FolderData = {
description: '', description: '',
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
updated_at: new Date().toISOString(), updated_at: new Date().toISOString(),
notes: [] notes: [],
messages: [],
messages_owner: []
}; };
function capitalize(s?: string) { function capitalize(s?: string) {
@ -32,7 +38,7 @@ function capitalize(s?: string) {
return s.charAt(0).toUpperCase() + s.slice(1); return s.charAt(0).toUpperCase() + s.slice(1);
} }
// Mapping des couleurs par type de dossier // Mapping des couleurs
const folderColors: Record<FolderType, { bg: string; border: string; focus: string; button: string }> = { const folderColors: Record<FolderType, { bg: string; border: string; focus: string; button: string }> = {
contrat: { bg: 'bg-blue-50 dark:bg-blue-900', border: 'border-blue-300 dark:border-blue-700', focus: 'focus:ring-blue-400 dark:focus:ring-blue-600', button: 'bg-blue-500 hover:bg-blue-600' }, contrat: { bg: 'bg-blue-50 dark:bg-blue-900', border: 'border-blue-300 dark:border-blue-700', focus: 'focus:ring-blue-400 dark:focus:ring-blue-600', button: 'bg-blue-500 hover:bg-blue-600' },
projet: { bg: 'bg-green-50 dark:bg-green-900', border: 'border-green-300 dark:border-green-700', focus: 'focus:ring-green-400 dark:focus:ring-green-600', button: 'bg-green-500 hover:bg-green-600' }, projet: { bg: 'bg-green-50 dark:bg-green-900', border: 'border-green-300 dark:border-green-700', focus: 'focus:ring-green-400 dark:focus:ring-green-600', button: 'bg-green-500 hover:bg-green-600' },
@ -51,15 +57,19 @@ function FolderModal({
isOpen, isOpen,
onClose, onClose,
folderType = 'autre', folderType = 'autre',
members = [],
renderExtraFields renderExtraFields
}: FolderModalProps) { }: FolderModalProps) {
const [folderData, setFolderData] = useState<FolderData>({ ...defaultFolder, ...folder }); const [folderData, setFolderData] = useState<FolderData>({ ...defaultFolder, ...folder });
const [currentNote, setCurrentNote] = useState(''); const [currentNote, setCurrentNote] = useState('');
// --- NOUVEAU: État pour les membres sélectionnés ---
const [selectedMembers, setSelectedMembers] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
setFolderData({ ...defaultFolder, ...(folder || {}) }); setFolderData({ ...defaultFolder, ...(folder || {}) });
setCurrentNote(''); setCurrentNote('');
setSelectedMembers([]); // <-- MODIFIÉ: Réinitialise les membres
} }
}, [isOpen, folder]); }, [isOpen, folder]);
@ -70,6 +80,14 @@ function FolderModal({
setFolderData(prev => ({ ...(prev as any), [name]: value } as FolderData)); setFolderData(prev => ({ ...(prev as any), [name]: value } as FolderData));
}; };
const handleMemberToggle = (memberId: string) => {
setSelectedMembers(prev =>
prev.includes(memberId)
? prev.filter(id => id !== memberId)
: [...prev, memberId]
);
};
const addNote = () => { const addNote = () => {
const v = currentNote.trim(); const v = currentNote.trim();
if (!v) return; if (!v) return;
@ -83,7 +101,7 @@ function FolderModal({
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
onSave?.({ ...folderData, updated_at: new Date().toISOString() }); onSave?.({ ...folderData, updated_at: new Date().toISOString() }, selectedMembers);
onClose(); onClose();
}; };
@ -139,6 +157,18 @@ function FolderModal({
</div> </div>
</div> </div>
{/* Membres */}
{!readOnly && (
<div className="space-y-4">
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Membres</h3>
<MemberAutocomplete
allMembers={members}
selectedMembers={selectedMembers}
onChange={setSelectedMembers} // On passe le setter de l'état
/>
</div>
)}
{/* Notes */} {/* Notes */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Notes</h3> <h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Notes</h3>

184
components/ui/command.tsx Normal file
View File

@ -0,0 +1,184 @@
"use client"
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton}
>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
)
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

143
components/ui/dialog.tsx Normal file
View File

@ -0,0 +1,143 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -0,0 +1,111 @@
"use client"
import * as React from "react"
import { Check, ChevronsUpDown, X } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { Badge } from "@/components/ui/badge"
interface MemberAutocompleteProps {
allMembers: string[];
selectedMembers: string[];
onChange: (selectedMembers: string[]) => void;
}
export function MemberAutocomplete({
allMembers,
selectedMembers,
onChange,
}: MemberAutocompleteProps) {
const [open, setOpen] = React.useState(false)
// Liste des membres qui ne sont PAS encore sélectionnés
const availableMembers = allMembers.filter(
(member) => !selectedMembers.includes(member)
)
// Gère la sélection d'un membre dans la liste
const handleSelect = (memberId: string) => {
onChange([...selectedMembers, memberId])
setOpen(false) // Ferme le popover après sélection
}
// Gère la suppression d'un membre (clic sur le 'X' du badge)
const handleRemove = (memberId: string) => {
onChange(selectedMembers.filter((m) => m !== memberId))
}
return (
<div className="space-y-2">
{/* 1. Affichage des membres déjà sélectionnés (Badges) */}
<div className="flex flex-wrap gap-1">
{selectedMembers.map((member) => (
<Badge
key={member}
variant="secondary"
className="flex items-center gap-1"
>
<span className="truncate max-w-[200px]" title={member}>{member}</span>
<button
type="button"
onClick={() => handleRemove(member)}
className="rounded-full hover:bg-red-500/20 p-0.5"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
{/* 2. Le Popover avec le bouton de recherche */}
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="button" // Important pour ne pas soumettre le formulaire
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between bg-gray-100 dark:bg-gray-700"
>
Ajouter un membre...
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
<Command>
<CommandInput placeholder="Rechercher un membre..." />
<CommandList>
<CommandEmpty>Aucun membre trouvé.</CommandEmpty>
<CommandGroup>
{availableMembers.map((member) => (
<CommandItem
key={member}
value={member} // 'value' est utilisé pour la recherche
onSelect={() => handleSelect(member)}
className="truncate"
>
{member}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
)
}

48
components/ui/popover.tsx Normal file
View File

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@ -489,7 +489,7 @@ export default class MessageBus {
}); });
} }
public updateProcess(processId: string, lastStateId: string, newData: Record<string, any>, privateFields: string[], roles: Record<string, RoleDefinition> | null): Promise<any> { public updateProcess(processId: string, newData: Record<string, any>, privateFields: string[], roles: Record<string, RoleDefinition> | null): Promise<any> {
return new Promise<any>((resolve: (updatedProcess: any) => void, reject: (error: string) => void) => { return new Promise<any>((resolve: (updatedProcess: any) => void, reject: (error: string) => void) => {
this.checkToken().then(() => { this.checkToken().then(() => {
const userStore = UserStore.getInstance(); const userStore = UserStore.getInstance();
@ -520,7 +520,6 @@ export default class MessageBus {
this.sendMessage({ this.sendMessage({
type: 'UPDATE_PROCESS', type: 'UPDATE_PROCESS',
processId, processId,
lastStateId,
newData, newData,
privateFields, privateFields,
roles, roles,
@ -624,7 +623,7 @@ export default class MessageBus {
return; return;
} }
console.log('[MessageBus] sendMessage:', message, 'to', this.origin); // console.log('[MessageBus] sendMessage:', message, 'to', this.origin);
iframe.contentWindow?.postMessage(message, this.origin); iframe.contentWindow?.postMessage(message, this.origin);
} }

View File

@ -7,6 +7,8 @@ export interface FolderData {
created_at: string; created_at: string;
updated_at: string; updated_at: string;
notes: string[]; notes: string[];
messages: string[];
messages_owner: string[];
} }
export function isFolderData(data: any): data is FolderData { export function isFolderData(data: any): data is FolderData {
@ -38,7 +40,9 @@ const emptyFolderData: FolderData = {
description: '', description: '',
created_at: '', created_at: '',
updated_at: '', updated_at: '',
notes: [] notes: [],
messages: [],
messages_owner: [],
}; };
const folderDataFields: string[] = Object.keys(emptyFolderData); const folderDataFields: string[] = Object.keys(emptyFolderData);
@ -72,7 +76,7 @@ export function setDefaultFolderRoles(ownerId: string): Record<string, RoleDefin
min_sig_member: 1, min_sig_member: 1,
}, },
], ],
storages: [] storages: ['https://dev2.4nkweb.com/storage']
}, },
apophis: { apophis: {
members: [ownerId], members: [ownerId],

View File

@ -6,19 +6,9 @@ import { iframeUrl } from "@/app/page";
import UserStore from "@/lib/4nk/UserStore"; import UserStore from "@/lib/4nk/UserStore";
import { FolderData } from "@/lib/4nk/models/FolderData"; import { FolderData } from "@/lib/4nk/models/FolderData";
// --- Définition des types pour plus de clarté ---
export interface FolderMember {
id: string
name: string
avatar: string
isOnline: boolean
}
// Interface enrichie qui inclut maintenant les membres ET les fichiers // Interface enrichie qui inclut maintenant les membres ET les fichiers
export interface EnrichedFolderData extends FolderData { export interface EnrichedFolderData extends FolderData {
members: FolderMember[]; processId: string,
files: any[]; // <-- AJOUT DES FICHIERS
// notes: any[]; // 'notes' est déjà dans FolderData
} }
// --- // ---
@ -30,12 +20,14 @@ type FourNKContextType = {
folderProcesses: any; folderProcesses: any;
myFolderProcesses: string[]; myFolderProcesses: string[];
folderPrivateData: Record<string, Record<string, any>>; folderPrivateData: Record<string, Record<string, any>>;
folders: EnrichedFolderData[]; // <-- Utilise le type enrichi folders: EnrichedFolderData[];
loadingFolders: boolean; loadingFolders: boolean;
members: string[];
setFolderProcesses: React.Dispatch<React.SetStateAction<any>>; setFolderProcesses: React.Dispatch<React.SetStateAction<any>>;
setMyFolderProcesses: React.Dispatch<React.SetStateAction<string[]>>; setMyFolderProcesses: React.Dispatch<React.SetStateAction<string[]>>;
setFolderPrivateData: React.Dispatch<React.SetStateAction<Record<string, Record<string, any>>>>; setFolderPrivateData: React.Dispatch<React.SetStateAction<Record<string, Record<string, any>>>>;
setMembers: React.Dispatch<React.SetStateAction<string[]>>;
}; };
const FourNKContext = createContext<FourNKContextType | undefined>(undefined); const FourNKContext = createContext<FourNKContextType | undefined>(undefined);
@ -44,6 +36,7 @@ export function FourNKProvider({ children }: { children: ReactNode }) {
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [userPairingId, setUserPairingId] = useState<string | null>(null); const [userPairingId, setUserPairingId] = useState<string | null>(null);
const [processes, setProcesses] = useState<any>(null); const [processes, setProcesses] = useState<any>(null);
const [members, setMembers] = useState<string[]>([]);;
const [myProcesses, setMyProcesses] = useState<string[]>([]); const [myProcesses, setMyProcesses] = useState<string[]>([]);
const [folderProcesses, setFolderProcesses] = useState<any>(null); const [folderProcesses, setFolderProcesses] = useState<any>(null);
const [myFolderProcesses, setMyFolderProcesses] = useState<string[]>([]); const [myFolderProcesses, setMyFolderProcesses] = useState<string[]>([]);
@ -73,58 +66,109 @@ export function FourNKProvider({ children }: { children: ReactNode }) {
let hasAllPrivateData = true; let hasAllPrivateData = true;
let hasFoldersToLoad = false; let hasFoldersToLoad = false;
const missingPrivateData: Array<{ processId: string, stateId: string }> = []; const missingPrivateData: Array<{ processId: string, stateId: string }> = [];
const EXCLUDED_STATE_ID = "0000000000000000000000000000000000000000000000000000000000000000";
Object.entries(folderProcesses).forEach(([processId, process]: [string, any]) => { Object.entries(folderProcesses).forEach(([processId, process]: [string, any]) => {
if (!myFolderProcesses.includes(processId)) return; if (!myFolderProcesses.includes(processId)) return;
const latestState = process.states[0]; const validStates = process.states.filter(
if (!latestState) return; state => state && state.state_id !== EXCLUDED_STATE_ID
const folderNumber = latestState.pcd_commitment?.folderNumber; );
if (!folderNumber) return; if (validStates.length === 0) return;
const referenceState = validStates.find(
hasFoldersToLoad = true; state => state.pcd_commitment?.folderNumber
const privateData = folderPrivateData[latestState.state_id]; );
const mainFolderNumber = referenceState?.pcd_commitment.folderNumber;
if (!privateData) { if (!mainFolderNumber) {
hasAllPrivateData = false;
missingPrivateData.push({ processId, stateId: latestState.state_id });
return; return;
} }
const ownerMembers = latestState.roles?.owner?.members || []; // console.log(validStates);
const members: FolderMember[] = ownerMembers.map((memberId: string) => {
const avatar = memberId.slice(0, 2).toUpperCase(); // validStates.forEach(state => {
return { hasFoldersToLoad = true;
id: memberId, const stateToProcess = referenceState;
name: `Membre ${memberId.slice(0, 4)}`,
avatar: avatar, const privateData = folderPrivateData[stateToProcess.state_id];
isOnline: Math.random() > 0.5
} // Si on n'a pas les données pour cet état de référence...
}); if (!privateData) {
hasAllPrivateData = false;
missingPrivateData.push({ processId, stateId: stateToProcess.state_id });
return; // On quitte ce process, on réessaiera au prochain rendu
}
// console.log("Données déchiffrées pour le state:", stateToProcess.state_id, privateData);
/*
// 4. CONDITION B: On vérifie si cet état contient des messages.
const hasMessages = (privateData.messages && privateData.messages.length > 0);
const hasMessagesOwner = (privateData.messages_owner && privateData.messages_owner.length > 0);
// Si cet état n'a pas de messages, on l'ignore (return)
if (!hasMessages && !hasMessagesOwner) {
return; // Passe à l'état suivant
}
*/
folderData.push({ folderData.push({
folderNumber: folderNumber, processId: processId,
name: privateData.name || `Dossier ${folderNumber}`, folderNumber: mainFolderNumber, // La clé unique
name: privateData.name || `Dossier ${mainFolderNumber}`,
description: privateData.description || '', description: privateData.description || '',
created_at: privateData.created_at || new Date().toISOString(), created_at: privateData.created_at || new Date().toISOString(),
updated_at: privateData.updated_at || new Date().toISOString(), updated_at: privateData.updated_at || new Date().toISOString(),
notes: privateData.notes || [], notes: privateData.notes || [],
files: privateData.files || [], // <-- AJOUT DE L'EXTRACTION DES FICHIERS messages: privateData.messages || [],
members: members messages_owner: privateData.messages_owner || [],
}); });
// });
}); });
if (hasFoldersToLoad && !hasAllPrivateData) { if (hasFoldersToLoad && !hasAllPrivateData) {
setLoadingFolders(true); setLoadingFolders(true);
missingPrivateData.forEach(({ processId, stateId }) => { const firstMissing = missingPrivateData[0];
if (!folderPrivateData[stateId]) { if (firstMissing) {
fetchFolderPrivateData(processId, stateId); if (!folderPrivateData[firstMissing.stateId]) {
fetchFolderPrivateData(firstMissing.processId, firstMissing.stateId);
} }
}); }
} else { } else {
setFolders(folderData); setFolders(folderData);
setLoadingFolders(false); setLoadingFolders(false);
} }
}, [folderProcesses, myFolderProcesses, folderPrivateData, fetchFolderPrivateData]); }, [folderProcesses, myFolderProcesses, folderPrivateData, fetchFolderPrivateData, setFolders, setLoadingFolders]); // J'ai ajouté setFolders et setLoadingFolders aux dépendances
const loadMembersFrom4NK = useCallback(() => {
if (!processes || !userPairingId) return;
const memberList: string[] = [];
const EXCLUDED_STATE_ID = "0000000000000000000000000000000000000000000000000000000000000000";
Object.entries(processes).forEach(([processId, process]: [string, any]) => {
const validStates = process.states.filter(
state => state && state.state_id !== EXCLUDED_STATE_ID
);
if (validStates.length === 0) return;
const referenceState = validStates.find(
state => state.pcd_commitment?.pairedAddresses
);
if (!referenceState) return;
const userAddress = referenceState.commited_in
memberList.push(userAddress);
});
// Filtrer la liste pour enlever l'ID de l'utilisateur connecté
const filteredMemberList = memberList.filter(
member => member !== userPairingId
);
// Sauvegarder la liste filtrée dans l'état
setMembers(filteredMemberList);
}, [processes, userPairingId]);
// Chargement initial des données 4NK // Chargement initial des données 4NK
useEffect(() => { useEffect(() => {
@ -176,6 +220,11 @@ export function FourNKProvider({ children }: { children: ReactNode }) {
loadFoldersFrom4NK(); loadFoldersFrom4NK();
}, [loadFoldersFrom4NK]); }, [loadFoldersFrom4NK]);
// Re-calculer les membres lorsque les données changent
useEffect(() => {
loadMembersFrom4NK();
}, [loadMembersFrom4NK]);
const value = { const value = {
isConnected, isConnected,
@ -187,9 +236,11 @@ export function FourNKProvider({ children }: { children: ReactNode }) {
folderPrivateData, folderPrivateData,
folders, folders,
loadingFolders, loadingFolders,
members,
setFolderProcesses, setFolderProcesses,
setMyFolderProcesses, setMyFolderProcesses,
setFolderPrivateData, setFolderPrivateData,
setMembers,
}; };
return ( return (

View File

@ -17,20 +17,20 @@
"@radix-ui/react-checkbox": "latest", "@radix-ui/react-checkbox": "latest",
"@radix-ui/react-collapsible": "1.1.2", "@radix-ui/react-collapsible": "1.1.2",
"@radix-ui/react-context-menu": "2.2.4", "@radix-ui/react-context-menu": "2.2.4",
"@radix-ui/react-dialog": "1.1.4", "@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "2.1.4", "@radix-ui/react-dropdown-menu": "2.1.4",
"@radix-ui/react-hover-card": "1.1.4", "@radix-ui/react-hover-card": "1.1.4",
"@radix-ui/react-label": "latest", "@radix-ui/react-label": "latest",
"@radix-ui/react-menubar": "1.1.4", "@radix-ui/react-menubar": "1.1.4",
"@radix-ui/react-navigation-menu": "1.2.3", "@radix-ui/react-navigation-menu": "1.2.3",
"@radix-ui/react-popover": "1.1.4", "@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-progress": "1.1.1", "@radix-ui/react-progress": "1.1.1",
"@radix-ui/react-radio-group": "latest", "@radix-ui/react-radio-group": "latest",
"@radix-ui/react-scroll-area": "1.2.2", "@radix-ui/react-scroll-area": "1.2.2",
"@radix-ui/react-select": "latest", "@radix-ui/react-select": "latest",
"@radix-ui/react-separator": "1.1.1", "@radix-ui/react-separator": "1.1.1",
"@radix-ui/react-slider": "1.2.2", "@radix-ui/react-slider": "1.2.2",
"@radix-ui/react-slot": "latest", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "latest", "@radix-ui/react-switch": "latest",
"@radix-ui/react-tabs": "1.1.2", "@radix-ui/react-tabs": "1.1.2",
"@radix-ui/react-toast": "1.2.4", "@radix-ui/react-toast": "1.2.4",
@ -41,6 +41,7 @@
"bip39": "^3.1.0", "bip39": "^3.1.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1",
"geist": "^1.3.1", "geist": "^1.3.1",
"lucide-react": "^0.454.0", "lucide-react": "^0.454.0",
"next": "15.2.4", "next": "15.2.4",

3655
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff