<?php
declare(strict_types=1);
function buildStructureMap(string $rootPath): array
{
$rootPath = rtrim($rootPath, '/');
$treeRoots = [
'pages',
'assets',
'css',
'js',
'api',
'setup',
'maps',
];
$tree = ['ROOT/'];
foreach (['index.php', 'index.html', 'login.php', 'menu.php'] as $rootFile) {
$rootFilePath = $rootPath . '/' . $rootFile;
$tree[] = ' ' . (is_file($rootFilePath)
? $rootFile . ' [' . formatKb((int) filesize($rootFilePath)) . ']'
: $rootFile . ' (missing)');
}
foreach ($treeRoots as $dir) {
$path = $rootPath . '/' . $dir;
if (is_dir($path)) {
$tree[] = $dir . '/';
appendTree($tree, $path, ' ', 2);
}
}
$pageDefinitions = [
['name' => 'Dashboard', 'php' => 'pages/dashboard.php', 'js' => 'js/dashboard.js', 'css' => 'css/dashboard.css', 'tags' => ['OLD', 'ACTIVE']],
['name' => 'Deals', 'php' => 'pages/deals.php', 'js' => 'js/deals.js', 'css' => 'css/deals.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'ProjectFlow', 'php' => 'pages/project-flow.php', 'js' => 'js/project-flow.js', 'css' => 'css/project-flow.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'Contractors', 'php' => 'pages/contractors.php', 'js' => 'js/contractors.js', 'css' => 'css/contractors.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'Purchasing', 'php' => 'pages/purchasing.php', 'js' => 'js/purchasing.js', 'css' => 'css/purchasing.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'Suppliers', 'php' => 'pages/suppliers.php', 'js' => 'js/suppliers.js', 'css' => 'css/suppliers.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'FieldSales', 'php' => 'pages/fieldsales.php', 'js' => 'js/fieldsales.js', 'css' => 'css/fieldsales.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'Leads', 'php' => 'pages/leads.php', 'js' => 'js/leads.js', 'css' => 'css/leads.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'Customers', 'php' => 'pages/customer.php', 'js' => 'js/customer.js', 'css' => 'css/customer.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'ProductList', 'php' => 'pages/productlist.php', 'js' => 'js/productlist.js', 'css' => 'css/productlist.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'CalcList', 'php' => 'pages/calc-list.php', 'js' => 'js/calc-list.js', 'css' => 'css/calc-list.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'NewCalc', 'php' => 'pages/kalkyl-ny-shell.php', 'js' => 'js/kalkyl-ny.js', 'css' => 'css/kalkyl-ny.css', 'tags' => ['OLD', 'ACTIVE']],
['name' => 'Calendar', 'php' => 'pages/calendar.php', 'js' => 'js/calendar.js', 'css' => 'css/calendar.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'Inbox', 'php' => 'pages/inbox.php', 'js' => 'js/inbox.js', 'css' => 'css/inbox.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'DailyReport', 'php' => 'pages/daily-report.php', 'js' => 'js/daily-report.js', 'css' => 'css/daily-report.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'MySalary', 'php' => 'pages/my-salary.php', 'js' => 'js/my-salary.js', 'css' => 'css/my-salary.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'Profile', 'php' => 'pages/profile.php', 'js' => 'js/profile.js', 'css' => 'css/profile.css', 'tags' => ['NEW', 'ACTIVE']],
['name' => 'Settings', 'php' => 'pages/settings.php', 'js' => 'js/settings.js', 'css' => 'css/settings.css', 'tags' => ['NEW', 'ACTIVE']],
];
$pages = [];
foreach ($pageDefinitions as $definition) {
$phpPath = $rootPath . '/' . $definition['php'];
$jsPath = $rootPath . '/' . $definition['js'];
$cssPath = $rootPath . '/' . $definition['css'];
$exists = [
'php' => is_file($phpPath),
'js' => is_file($jsPath),
'css' => is_file($cssPath),
];
$count = count(array_filter($exists));
$status = 'Missing';
$statusClass = 'status-missing';
if ($count === 3) {
$status = 'OK';
$statusClass = 'status-ok';
} elseif ($count > 0) {
$status = 'Partial';
$statusClass = 'status-partial';
}
$pages[] = [
'name' => $definition['name'],
'php' => formatFileEntry($definition['php'], $phpPath, $definition['tags']),
'js' => formatFileEntry($definition['js'], $jsPath, $definition['tags']),
'css' => formatFileEntry($definition['css'], $cssPath, $definition['tags']),
'status' => $status,
'statusClass' => $statusClass,
];
}
$keyFiles = [];
foreach ([
['path' => 'index.php', 'tags' => ['OLD', 'ACTIVE']],
['path' => 'index.html', 'tags' => ['OLD', 'ACTIVE']],
['path' => 'login.php', 'tags' => ['NEW', 'TARGET']],
['path' => 'menu.php', 'tags' => ['NEW', 'TARGET']],
] as $fileDef) {
$keyFiles[] = formatFileEntry($fileDef['path'], $rootPath . '/' . $fileDef['path'], $fileDef['tags']);
}
$counts = [];
foreach (['pages', 'js', 'css', 'api', 'setup', 'maps'] as $dir) {
$dirPath = $rootPath . '/' . $dir;
$counts[] = [
'dir' => $dir,
'count' => is_dir($dirPath) ? countPhpJsCssFiles($dirPath) : 0,
'size' => is_dir($dirPath) ? formatKb((int) directorySize($dirPath)) : '0 KB',
];
}
$targetStructure = [
formatTargetEntry('index.php', ['NEW', 'TARGET'], 'Shell entry point'),
formatTargetEntry('login.php', ['NEW', 'TARGET'], 'Login controls user identity and page permissions'),
formatTargetEntry('menu.php', ['NEW', 'TARGET'], 'Shared menu shell for all pages'),
formatTargetEntry('pages/dashboard.php', ['NEW', 'TARGET'], 'Dashboard filtered by logged-in user later'),
formatTargetEntry('pages/deals.php', ['NEW', 'TARGET'], 'Deals page'),
formatTargetEntry('pages/project-flow.php', ['NEW', 'TARGET'], 'ProjectFlow page'),
formatTargetEntry('pages/contractors.php', ['NEW', 'TARGET'], 'Contractors page'),
formatTargetEntry('pages/purchasing.php', ['NEW', 'TARGET'], 'Purchasing page'),
formatTargetEntry('pages/suppliers.php', ['NEW', 'TARGET'], 'Suppliers page'),
formatTargetEntry('pages/fieldsales.php', ['NEW', 'TARGET'], 'FieldSales page'),
formatTargetEntry('pages/leads.php', ['NEW', 'TARGET'], 'Leads page'),
formatTargetEntry('pages/customer.php', ['NEW', 'TARGET'], 'Customers page'),
formatTargetEntry('pages/productlist.php', ['NEW', 'TARGET'], 'ProductList page'),
formatTargetEntry('pages/calc-list.php', ['NEW', 'TARGET'], 'CalcList page'),
formatTargetEntry('pages/new-calc.php', ['NEW', 'TARGET'], 'NewCalc page'),
formatTargetEntry('pages/calendar.php', ['NEW', 'TARGET'], 'Calendar tied to logged-in user'),
formatTargetEntry('pages/inbox.php', ['NEW', 'TARGET'], 'Inbox tied to logged-in user'),
formatTargetEntry('pages/daily-report.php', ['NEW', 'TARGET'], 'Personal DailyReport for logged-in user'),
formatTargetEntry('pages/my-salary.php', ['NEW', 'TARGET'], 'Personal MySalary for logged-in user'),
formatTargetEntry('pages/profile.php', ['NEW', 'TARGET'], 'Profile page in bottom area with user settings'),
formatTargetEntry('pages/settings.php', ['NEW', 'TARGET'], 'Settings page with company, AI, Google, notifications and finance tabs'),
formatTargetEntry('assets/js/', ['NEW', 'TARGET'], 'Canonical JS folder'),
formatTargetEntry('assets/css/', ['NEW', 'TARGET'], 'Canonical CSS folder'),
formatTargetEntry('api/', ['NEW', 'TARGET'], 'Shared API directory'),
formatTargetEntry('setup/', ['NEW', 'TARGET'], 'Settings and structure scripts'),
formatTargetEntry('maps/', ['NEW', 'TARGET'], 'Structure map, tasks, and bug tracking'),
];
$preferences = [
'Profile page should allow user to edit profile data',
'Profile page should allow toggling Calendar on/off',
'Profile page should allow toggling Inbox on/off',
'Calendar and Inbox are created from login identity',
'DailyReport and MySalary are always personal to the logged-in user',
];
$legacyFiles = [];
foreach (findLegacyFiles($rootPath) as $legacyFile) {
$legacyFiles[] = formatFileEntry($legacyFile, $rootPath . '/' . $legacyFile, ['OLD', 'BACKUP']);
}
return [
'rootLabel' => basename($rootPath),
'tree' => implode("\n", $tree),
'pages' => $pages,
'keyFiles' => $keyFiles,
'counts' => $counts,
'targetStructure' => $targetStructure,
'preferences' => $preferences,
'legacyFiles' => $legacyFiles,
];
}
function appendTree(array &$lines, string $path, string $prefix, int $depth): void
{
if ($depth < 0 || !is_dir($path)) {
return;
}
$items = scandir($path);
if ($items === false) {
return;
}
$items = array_values(array_filter($items, static function (string $item): bool {
return $item !== '.' && $item !== '..';
}));
sort($items);
foreach ($items as $item) {
$fullPath = $path . '/' . $item;
$isDir = is_dir($fullPath);
$label = $isDir ? $item . '/' : $item . ' [' . formatKb(is_file($fullPath) ? (int) filesize($fullPath) : 0) . ']';
$lines[] = $prefix . $label;
if ($isDir) {
appendTree($lines, $fullPath, $prefix . ' ', $depth - 1);
}
}
}
function countPhpJsCssFiles(string $path): int
{
$count = 0;
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS));
foreach ($iterator as $fileInfo) {
if ($fileInfo->isFile()) {
$extension = strtolower($fileInfo->getExtension());
if (in_array($extension, ['php', 'js', 'css'], true)) {
$count++;
}
}
}
return $count;
}
function directorySize(string $path): int
{
$size = 0;
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS));
foreach ($iterator as $fileInfo) {
if ($fileInfo->isFile()) {
$size += (int) $fileInfo->getSize();
}
}
return $size;
}
function formatFileEntry(string $relativePath, string $absolutePath, array $tags): array
{
return [
'path' => $relativePath,
'code' => buildFileCode($relativePath),
'label' => is_file($absolutePath) ? $relativePath . ' [' . formatKb((int) filesize($absolutePath)) . ']' : $relativePath . ' (missing)',
'exists' => is_file($absolutePath) || is_dir($absolutePath),
'isFile' => is_file($absolutePath),
'tags' => $tags,
];
}
function formatTargetEntry(string $path, array $tags, string $note): array
{
return [
'path' => $path,
'code' => buildFileCode($path),
'tags' => $tags,
'note' => $note,
];
}
function buildFileCode(string $relativePath): string
{
return 'DEV-' . strtoupper(substr(sha1($relativePath), 0, 8));
}
function formatKb(int $bytes): string
{
if ($bytes <= 0) {
return '0 KB';
}
return number_format($bytes / 1024, 1, '.', '') . ' KB';
}
function findLegacyFiles(string $rootPath): array
{
$legacy = [];
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($rootPath, FilesystemIterator::SKIP_DOTS));
foreach ($iterator as $fileInfo) {
if (!$fileInfo->isFile()) {
continue;
}
$relative = ltrim(str_replace($rootPath, '', $fileInfo->getPathname()), '/');
if (preg_match('/(\.bak|backup|index-broken|index-today)/i', $relative)) {
$legacy[] = $relative;
}
}
sort($legacy);
return $legacy;
}