[TSM.ID].[11031972] PXE : Platform X Ecosystem I [118 Module -LIVE-]
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
// [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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user