Files
multiverse/xcom-ultra/xcu-command-center/src/components/TenantModuleMatrix.jsx
T

231 lines
11 KiB
React

// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
import { useState, useMemo } from 'react';
import { useModuleRegistry } from '../context/ModuleRegistryContext';
import { useCommercial } from '../context/CommercialContext';
export default function TenantModuleMatrix({ allowedModules = [] }) {
const { registry, allModules } = useModuleRegistry();
const { tiers, modulePrices } = useCommercial();
const [searchQuery, setSearchQuery] = useState('');
const [viewMode, setViewMode] = useState('grid'); // grid | list
// Supreme mode: all modules granted
const isSupreme = allowedModules === 'supreme';
const effectiveAllowed = isSupreme ? allModules.map(m => m.id) : allowedModules;
// Build module status map
const moduleStatusMap = useMemo(() => {
const map = {};
allModules.forEach(mod => {
const isGranted = effectiveAllowed.includes(mod.id);
map[mod.id] = {
...mod,
granted: isGranted,
price: modulePrices?.[mod.id] || null
};
});
return map;
}, [allModules, allowedModules, modulePrices]);
const grantedCount = effectiveAllowed.length;
const totalCount = allModules.length;
const lockedCount = totalCount - grantedCount;
// Find matching tier
const matchedTier = useMemo(() => {
return tiers.find(t =>
t.modules.length === effectiveAllowed.length &&
t.modules.every(m => effectiveAllowed.includes(m))
);
}, [tiers, allowedModules]);
// Filter modules
const filteredModules = useMemo(() => {
if (!searchQuery.trim()) return allModules;
const q = searchQuery.toLowerCase();
return allModules.filter(m =>
m.id.toLowerCase().includes(q) ||
m.name.toLowerCase().includes(q)
);
}, [allModules, searchQuery]);
// Group by registry category
const groupedModules = useMemo(() => {
const groups = {};
for (const [groupKey, groupData] of Object.entries(registry)) {
const mods = groupData.modules.filter(m =>
filteredModules.some(fm => fm.id === m.id)
);
if (mods.length > 0) {
groups[groupKey] = { ...groupData, modules: mods };
}
}
return groups;
}, [registry, filteredModules]);
return (
<div className="glass-panel" style={{padding: '30px', borderColor: 'var(--accent-purple)'}}>
{/* Header Stats */}
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: '15px', marginBottom: '25px'}}>
<div>
<h2 style={{color: 'var(--accent-purple)', marginBottom: '8px', fontFamily: 'monospace', letterSpacing: '2px'}}>MODULE ACCESS MATRIX</h2>
<p style={{color: 'var(--text-muted)', fontSize: '0.85rem'}}>
{isSupreme && <span style={{color: 'var(--accent-yellow)', fontWeight: 'bold'}}>SUPREME ADMIN ALL ACCESS</span>}{!isSupreme && <>Tier: <span style={{color: 'var(--accent-cyan)', fontWeight: 'bold'}}>{matchedTier ? matchedTier.name : 'CUSTOM A LA CARTE'}</span></>}
</p>
</div>
<div style={{display: 'flex', gap: '12px'}}>
<div style={{background: 'rgba(0, 255, 136, 0.08)', border: '1px solid var(--accent-green)', padding: '10px 18px', borderRadius: '8px', fontFamily: 'monospace', textAlign: 'center'}}>
<div style={{color: 'var(--accent-green)', fontSize: '1.6rem', fontWeight: 'bold'}}>{grantedCount}</div>
<div style={{color: 'var(--text-muted)', fontSize: '0.6rem', letterSpacing: '1px'}}>GRANTED</div>
</div>
<div style={{background: 'rgba(255, 0, 60, 0.08)', border: '1px solid var(--accent-red)', padding: '10px 18px', borderRadius: '8px', fontFamily: 'monospace', textAlign: 'center'}}>
<div style={{color: 'var(--accent-red)', fontSize: '1.6rem', fontWeight: 'bold'}}>{lockedCount}</div>
<div style={{color: 'var(--text-muted)', fontSize: '0.6rem', letterSpacing: '1px'}}>LOCKED</div>
</div>
<div style={{background: 'rgba(0, 243, 255, 0.08)', border: '1px solid var(--accent-cyan)', padding: '10px 18px', borderRadius: '8px', fontFamily: 'monospace', textAlign: 'center'}}>
<div style={{color: 'var(--accent-cyan)', fontSize: '1.6rem', fontWeight: 'bold'}}>{totalCount}</div>
<div style={{color: 'var(--text-muted)', fontSize: '0.6rem', letterSpacing: '1px'}}>TOTAL</div>
</div>
</div>
</div>
{/* Search + View Toggle */}
<div style={{display: 'flex', gap: '10px', marginBottom: '25px', alignItems: 'center'}}>
<input
type="text"
placeholder="🔍 Cari modul (ID atau nama)..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{
flex: 1, padding: '10px 15px', background: 'var(--panel-bg)', color: 'var(--text-main)',
border: '1px solid var(--table-border)', borderRadius: '8px', fontFamily: 'monospace', fontSize: '0.85rem'
}}
/>
<div style={{display: 'flex', border: '1px solid var(--table-border)', borderRadius: '6px', overflow: 'hidden'}}>
<button
onClick={() => setViewMode('grid')}
style={{
padding: '8px 14px', background: viewMode === 'grid' ? 'var(--accent-purple)' : 'transparent',
color: viewMode === 'grid' ? '#fff' : 'var(--text-muted)', border: 'none', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.8rem'
}}
> Grid</button>
<button
onClick={() => setViewMode('list')}
style={{
padding: '8px 14px', background: viewMode === 'list' ? 'var(--accent-purple)' : 'transparent',
color: viewMode === 'list' ? '#fff' : 'var(--text-muted)', border: 'none', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.8rem'
}}
> List</button>
</div>
</div>
{/* Progress Bar */}
<div style={{marginBottom: '25px'}}>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom: '5px'}}>
<span style={{color: 'var(--text-muted)', fontSize: '0.75rem', fontFamily: 'monospace'}}>COVERAGE</span>
<span style={{color: 'var(--accent-green)', fontSize: '0.75rem', fontFamily: 'monospace'}}>{totalCount > 0 ? Math.round(grantedCount / totalCount * 100) : 0}%</span>
</div>
<div style={{width: '100%', height: '6px', background: 'rgba(255,255,255,0.05)', borderRadius: '3px', overflow: 'hidden'}}>
<div style={{
width: totalCount > 0 ? `${grantedCount / totalCount * 100}%` : '0%',
height: '100%',
background: 'linear-gradient(90deg, var(--accent-green), var(--accent-cyan))',
borderRadius: '3px',
transition: 'width 0.5s ease'
}} />
</div>
</div>
{/* Module Grid/List by Group */}
{Object.entries(groupedModules).map(([groupKey, groupData]) => (
<div key={groupKey} style={{marginBottom: '25px'}}>
<h3 style={{
color: 'var(--accent-cyan)', fontSize: '0.8rem', fontFamily: 'monospace',
letterSpacing: '2px', marginBottom: '12px', textTransform: 'uppercase',
borderBottom: '1px solid rgba(0, 243, 255, 0.15)', paddingBottom: '6px'
}}>
{groupKey.replace(/_/g, ' ')} ({groupData.modules.length})
</h3>
{viewMode === 'grid' ? (
<div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: '10px'}}>
{groupData.modules.map(mod => {
const status = moduleStatusMap[mod.id];
const isGranted = status?.granted;
return (
<div key={mod.id} style={{
padding: '14px', borderRadius: '8px',
background: isGranted ? 'rgba(0, 255, 136, 0.04)' : 'rgba(255, 0, 60, 0.03)',
border: `1px solid ${isGranted ? 'rgba(0, 255, 136, 0.25)' : 'rgba(255, 0, 60, 0.15)'}`,
opacity: isGranted ? 1 : 0.5,
transition: 'all 0.2s'
}}>
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px'}}>
<span style={{color: isGranted ? 'var(--accent-green)' : 'var(--accent-red)', fontFamily: 'monospace', fontSize: '0.7rem', fontWeight: 'bold'}}>
{mod.id.toUpperCase()}
</span>
<span style={{
fontSize: '0.55rem', fontFamily: 'monospace', fontWeight: 'bold',
padding: '2px 6px', borderRadius: '3px',
background: isGranted ? 'rgba(0, 255, 136, 0.15)' : 'rgba(255, 0, 60, 0.15)',
color: isGranted ? 'var(--accent-green)' : 'var(--accent-red)'
}}>
{isGranted ? '✓ GRANTED' : '✗ LOCKED'}
</span>
</div>
<div style={{color: 'var(--text-main)', fontSize: '0.78rem', lineHeight: 1.3}}>
{mod.name}
</div>
{status?.price && (
<div style={{color: 'var(--accent-yellow)', fontSize: '0.65rem', fontFamily: 'monospace', marginTop: '4px'}}>
${status.price}/mo
</div>
)}
</div>
);
})}
</div>
) : (
<table style={{width: '100%', borderCollapse: 'collapse'}}>
<tbody>
{groupData.modules.map(mod => {
const status = moduleStatusMap[mod.id];
const isGranted = status?.granted;
return (
<tr key={mod.id} style={{borderBottom: '1px solid rgba(255,255,255,0.04)'}}>
<td style={{padding: '8px 12px', fontFamily: 'monospace', fontSize: '0.75rem', color: isGranted ? 'var(--accent-green)' : 'var(--accent-red)', width: '60px'}}>
{mod.id}
</td>
<td style={{padding: '8px 12px', color: 'var(--text-main)', fontSize: '0.8rem', opacity: isGranted ? 1 : 0.4}}>
{mod.name}
</td>
<td style={{padding: '8px 12px', textAlign: 'right'}}>
<span style={{
fontSize: '0.6rem', fontFamily: 'monospace', fontWeight: 'bold',
padding: '3px 8px', borderRadius: '3px',
background: isGranted ? 'rgba(0, 255, 136, 0.12)' : 'rgba(255, 0, 60, 0.12)',
color: isGranted ? 'var(--accent-green)' : 'var(--accent-red)'
}}>
{isGranted ? '✓ GRANTED' : '✗ LOCKED'}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
))}
{filteredModules.length === 0 && (
<div style={{textAlign: 'center', padding: '40px', color: 'var(--text-muted)', fontFamily: 'monospace'}}>
Tidak ada modul yang cocok dengan pencarian.
</div>
)}
</div>
);
}