Tech4biz-channel/Channel-Frontend/src/features/assets/components/ShareAssetModal.tsx
2026-07-15 14:28:24 +05:30

260 lines
10 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { updateAsset, bulkShareAssets, getAssetGroups, addAssetsToGroup } from '../../../services/assets-api';
import type { AssetGroup } from '../../../services/assets-api';
import type { Asset, Organization, ShareItem } from '../../../types/assets';
import Modal from '../../../components/ui/Modal';
import Button from '../../../components/ui/Button';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useToast } from '../../../hooks/use-toast';
interface ShareAssetModalProps {
isOpen: boolean;
onClose: () => void;
asset: Asset | null;
assetIds?: string[] | null;
organizations: Organization[];
onSuccess: () => void;
}
export const ShareAssetModal: React.FC<ShareAssetModalProps> = ({
isOpen,
onClose,
asset,
assetIds,
organizations,
onSuccess
}) => {
const { success, error } = useToast();
const [sharesList, setSharesList] = useState<ShareItem[]>([]);
const [isSavingShare, setIsSavingShare] = useState(false);
const [expandedOrgId, setExpandedOrgId] = useState<string | null>(null);
const [groups, setGroups] = useState<AssetGroup[]>([]);
const [selectedGroupId, setSelectedGroupId] = useState<string>('');
useEffect(() => {
if (isOpen) {
getAssetGroups().then(setGroups).catch(console.error);
setSelectedGroupId('');
}
}, [isOpen]);
useEffect(() => {
if (asset) {
setSharesList(
asset.sharedWith?.map(s => ({
organizationId: s.organizationId,
userId: s.userId
})) || []
);
} else {
setSharesList([]);
}
}, [asset, isOpen]);
const isOrgSharedEntirely = (orgId: string) => {
return sharesList.some(s => s.organizationId === orgId && s.userId === null);
};
const isUserSharedSpecifically = (orgId: string, userId: string) => {
return sharesList.some(s => s.organizationId === orgId && s.userId === userId);
};
const handleToggleOrg = (orgId: string) => {
const isShared = isOrgSharedEntirely(orgId);
if (isShared) {
setSharesList(prev => prev.filter(s => s.organizationId !== orgId));
} else {
setSharesList(prev => [
...prev.filter(s => s.organizationId !== orgId),
{ organizationId: orgId, userId: null }
]);
}
};
const handleToggleUser = (orgId: string, userId: string) => {
const isShared = isUserSharedSpecifically(orgId, userId);
if (isShared) {
setSharesList(prev => prev.filter(s => !(s.organizationId === orgId && s.userId === userId)));
} else {
setSharesList(prev => [
...prev.filter(s => !(s.organizationId === orgId && s.userId === null)),
{ organizationId: orgId, userId }
]);
}
};
const handleShareSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!asset && (!assetIds || assetIds.length === 0)) return;
setIsSavingShare(true);
try {
if (asset) {
await updateAsset(asset.id, {
shares: sharesList
});
} else if (assetIds && assetIds.length > 0) {
await bulkShareAssets(assetIds, sharesList);
}
if (selectedGroupId) {
const ids = asset ? [asset.id] : (assetIds || []);
if (ids.length > 0) {
await addAssetsToGroup(selectedGroupId, ids);
}
}
success('Share permissions updated', 'The asset visibility and group membership settings have been updated.');
onSuccess();
onClose();
} catch (err: any) {
console.error('Failed to update share permissions', err);
error('Failed to update share permissions', err.response?.data?.error || 'Something went wrong.');
} finally {
setIsSavingShare(false);
}
};
return (
<Modal
isOpen={isOpen && (!!asset || (!!assetIds && assetIds.length > 0))}
onClose={onClose}
title="Share Settings"
subtitle={asset ? asset.title : `${assetIds?.length || 0} selected assets`}
size="md"
footer={
<>
<Button
type="button"
onClick={onClose}
variant="ghost"
size="sm"
>
Cancel
</Button>
<Button
type="submit"
form="share-asset-form"
disabled={isSavingShare}
variant="primary"
size="sm"
>
{isSavingShare ? 'Saving...' : 'Update Shares'}
</Button>
</>
}
>
{(asset || (assetIds && assetIds.length > 0)) && (
<form id="share-asset-form" onSubmit={handleShareSubmit} className="space-y-4">
<p className="text-xs text-ink-600 leading-relaxed font-sans">
Select organizations or expand to specify exact users that can access this asset:
</p>
<div className="max-h-64 overflow-y-auto border border-ink-200 rounded-xl divide-y divide-ink-250 bg-ink-50 scrollbar-thin">
{organizations.length === 0 ? (
<p className="p-4 text-xs text-ink-500 text-center font-medium font-sans">No partner organizations registered yet.</p>
) : (
organizations.map(org => {
const isEntireShared = isOrgSharedEntirely(org.id);
const isExpanded = expandedOrgId === org.id;
const activeUsers = org.users || [];
const specificSharedCount = sharesList.filter(s => s.organizationId === org.id && s.userId !== null).length;
return (
<div key={org.id} className="flex flex-col">
<div className="flex items-center justify-between p-3 hover:bg-ink-100 transition-colors">
<label className="flex items-center gap-3 cursor-pointer flex-1 select-none">
<input
type="checkbox"
checked={isEntireShared}
onChange={() => handleToggleOrg(org.id)}
className="w-4 h-4 rounded border-ink-300 text-ink-900 focus:ring-ink-900/10 cursor-pointer"
/>
<div className="flex flex-col">
<span className="text-xs font-bold text-ink-900 font-sans">{org.name}</span>
{specificSharedCount > 0 && !isEntireShared && (
<span className="text-[10px] text-ink-500 font-semibold font-sans">
Shared with {specificSharedCount} specific {specificSharedCount === 1 ? 'user' : 'users'}
</span>
)}
</div>
</label>
<button
type="button"
onClick={() => setExpandedOrgId(isExpanded ? null : org.id)}
className="p-1 rounded-lg text-ink-400 hover:text-ink-900 hover:bg-ink-200 transition-colors flex items-center gap-1 text-[11px] font-bold cursor-pointer font-sans"
>
<span>Users</span>
{isExpanded ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
</button>
</div>
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="bg-ink-100 border-t border-b border-ink-200 overflow-hidden divide-y divide-ink-150"
>
{activeUsers.length === 0 ? (
<p className="p-3 text-[10px] text-ink-500 italic font-sans">No users found in this organization.</p>
) : (
activeUsers.map(userItem => {
const isUserShared = isUserSharedSpecifically(org.id, userItem.id);
return (
<label key={userItem.id} className="flex items-center gap-3 py-2 px-8 cursor-pointer hover:bg-ink-200/50 transition-all select-none">
<input
type="checkbox"
disabled={isEntireShared}
checked={isEntireShared || isUserShared}
onChange={() => handleToggleUser(org.id, userItem.id)}
className="w-3.5 h-3.5 rounded border-ink-300 text-ink-900 focus:ring-ink-900/10 cursor-pointer disabled:opacity-50"
/>
<span className={`text-[11px] font-semibold font-sans ${isEntireShared ? 'text-ink-400' : 'text-ink-800'}`}>
{userItem.email}
</span>
</label>
);
})
)}
</motion.div>
)}
</AnimatePresence>
</div>
);
})
)}
</div>
{groups.length > 0 && (
<div className="pt-2 border-t border-ink-200">
<label className="text-[10px] font-bold text-ink-500 uppercase tracking-wide mb-1 block">
Add to Asset Group / Bundle (Optional)
</label>
<select
value={selectedGroupId}
onChange={(e) => setSelectedGroupId(e.target.value)}
className="w-full px-3 py-2 bg-ink-0 border border-ink-200 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 font-semibold"
>
<option value="">-- Select Group --</option>
{groups.map((g) => (
<option key={g.id} value={g.id}>
{g.name}
</option>
))}
</select>
<p className="text-[9px] text-ink-450 mt-1 font-medium">
This will automatically register the shared asset(s) as part of the selected bundle.
</p>
</div>
)}
</form>
)}
</Modal>
);
};