1,'Feb'=>2,'Mar'=>3,'Apr'=>4,'May'=>5,'Jun'=>6, 'Jul'=>7,'Aug'=>8,'Sep'=>9,'Oct'=>10,'Nov'=>11,'Dec'=>12 ]; if (strtoupper($label) === 'TOTAL') { return ['year'=>null,'month_num'=>null,'is_total'=>1,'is_partial'=>0]; } // Partial month: "May 1 - 11, 26" if (preg_match('/^(\w{3})\s+\d+\s*-\s*\d+,\s*(\d{2})$/', $label, $m)) { $mn = $months[$m[1]] ?? null; if (!$mn) return null; $yy = (int)$m[2]; return ['year'=>$yy>=90?1900+$yy:2000+$yy,'month_num'=>$mn,'is_total'=>0,'is_partial'=>1]; } // Standard: "May 90" if (preg_match('/^(\w{3})\s+(\d{2})$/', $label, $m)) { $mn = $months[$m[1]] ?? null; if (!$mn) return null; $yy = (int)$m[2]; return ['year'=>$yy>=90?1900+$yy:2000+$yy,'month_num'=>$mn,'is_total'=>0,'is_partial'=>0]; } return null; } function importCSV(string $filepath, string $dbPath): array { if (!file_exists($filepath) || !is_readable($filepath)) { return ['ok'=>false,'msg'=>'File not found or not readable: '.$filepath]; } $dir = dirname($dbPath); if (!is_dir($dir) && !mkdir($dir, 0775, true)) { return ['ok'=>false,'msg'=>'Cannot create database directory.']; } try { $db = new PDO('sqlite:'.$dbPath); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (Exception $e) { return ['ok'=>false,'msg'=>'Cannot open database: '.$e->getMessage()]; } $db->exec('PRAGMA journal_mode=WAL'); $db->exec('DROP TABLE IF EXISTS pl_values'); $db->exec('DROP TABLE IF EXISTS accounts'); $db->exec('DROP TABLE IF EXISTS months'); $db->exec('CREATE TABLE months ( id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT NOT NULL, year INTEGER, month_num INTEGER, sort_order INTEGER NOT NULL, is_total INTEGER NOT NULL DEFAULT 0, is_partial INTEGER NOT NULL DEFAULT 0 )'); $db->exec('CREATE TABLE accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, row_type TEXT NOT NULL, indent_level INTEGER NOT NULL DEFAULT 0, sort_order INTEGER NOT NULL )'); $db->exec('CREATE TABLE pl_values ( account_id INTEGER NOT NULL, month_id INTEGER NOT NULL, value REAL NOT NULL DEFAULT 0, PRIMARY KEY (account_id, month_id) )'); $handle = fopen($filepath, 'r'); if (!$handle) return ['ok'=>false,'msg'=>'Cannot open file.']; // Strip UTF-8 BOM if present $bom = fread($handle, 3); if ($bom !== "\xEF\xBB\xBF") rewind($handle); $header = fgetcsv($handle); if (!$header) { fclose($handle); return ['ok'=>false,'msg'=>'Empty or invalid CSV.']; } $db->beginTransaction(); // Insert months from header (col 0 is the row-label column, skip it) $monthStmt = $db->prepare( 'INSERT INTO months (label, year, month_num, sort_order, is_total, is_partial) VALUES (?,?,?,?,?,?)' ); $monthIds = []; // col_index => month.id for ($i = 1; $i < count($header); $i++) { $label = trim($header[$i]); if ($label === '') continue; $parsed = parseMonthLabel($label); if (!$parsed || $parsed['is_partial']) continue; if ($parsed['year'] !== null && $parsed['year'] < 1996) continue; $monthStmt->execute([ $label, $parsed['year'], $parsed['month_num'], $i, $parsed['is_total'], $parsed['is_partial'] ]); $monthIds[$i] = (int)$db->lastInsertId(); } // Known row classifications $level0Headers = ['Ordinary Income/Expense','Other Income/Expense']; $level1Headers = ['Income','Cost of Goods Sold','Expense','Other Income','Other Expense']; $level0Subtotals = ['Gross Profit','Net Ordinary Income','Net Other Income']; $level1Subtotals = ['Total Income','Total COGS','Total Expense','Total Other Income','Total Other Expense']; $totals = ['Net Income']; $accountStmt = $db->prepare( 'INSERT INTO accounts (name, row_type, indent_level, sort_order) VALUES (?,?,?,?)' ); $valueStmt = $db->prepare( 'INSERT OR REPLACE INTO pl_values (account_id, month_id, value) VALUES (?,?,?)' ); $sortOrder = 0; $accountCount = 0; $valueCount = 0; while (($row = fgetcsv($handle)) !== false) { if (empty($row)) continue; $name = trim($row[0] ?? ''); if ($name === '') continue; // Detect whether any value columns are non-empty $hasValues = false; for ($i = 1; $i < count($row); $i++) { if (isset($row[$i]) && trim($row[$i]) !== '') { $hasValues = true; break; } } if (!$hasValues) { $rowType = 'header'; $indent = in_array($name, $level0Headers) ? 0 : 1; } elseif (in_array($name, $totals)) { $rowType = 'total'; $indent = 0; } elseif (in_array($name, $level0Subtotals)) { $rowType = 'subtotal'; $indent = 0; } elseif (in_array($name, $level1Subtotals)) { $rowType = 'subtotal'; $indent = 1; } else { $rowType = 'item'; $indent = 2; } $accountStmt->execute([$name, $rowType, $indent, $sortOrder++]); $accountId = (int)$db->lastInsertId(); $accountCount++; if ($hasValues) { for ($i = 1; $i < count($row); $i++) { if (!isset($monthIds[$i])) continue; $raw = trim($row[$i] ?? ''); if ($raw === '') continue; $val = (float)str_replace([',', ' '], '', $raw); $valueStmt->execute([$accountId, $monthIds[$i], $val]); $valueCount++; } } } $db->commit(); fclose($handle); $numMonths = count($monthIds); return [ 'ok' => true, 'msg' => "Successfully imported $accountCount accounts across $numMonths months ($valueCount data points)." ]; } $configPath = __DIR__ . '/_private/config.php'; function loadCodes(string $path): array { return file_exists($path) ? (require $path) : []; } function saveCodes(string $path, array $codes): void { $export = " $label, 'hash' => password_hash($pw, PASSWORD_BCRYPT)]; saveCodes($configPath, $codes); $accessMsg = "Access code for \"" . htmlspecialchars($label) . "\" added."; $accessType = 'success'; } } elseif (isset($_POST['remove_code'])) { $idx = (int)$_POST['remove_code']; $codes = loadCodes($configPath); if (count($codes) <= 1) { $accessMsg = 'Cannot remove the last access code.'; $accessType = 'error'; } elseif (isset($codes[$idx])) { $label = $codes[$idx]['label']; array_splice($codes, $idx, 1); saveCodes($configPath, $codes); $accessMsg = "Access code for \"" . htmlspecialchars($label) . "\" removed."; $accessType = 'success'; } } else { $uploadErr = $_FILES['csvfile']['error'] ?? -1; $message = 'Upload error (code '.$uploadErr.'). Check php.ini upload_max_filesize.'; $messageType = 'error'; } } $hasExistingCsv = file_exists(__DIR__ . '/May2026PL.csv'); $hasDb = file_exists($dbPath) && filesize($dbPath) > 0; ?>
Found May2026PL.csv on this server. Click to import it directly.
Upload a QuickBooks P&L export (by month). All existing data will be replaced.
| # | Label | |
|---|---|---|
| = $i + 1 ?> | = htmlspecialchars($code['label']) ?> |