| Server IP : 173.209.174.21 / Your IP : 216.73.216.89 Web Server : Apache/2.4.58 (Ubuntu) System : Linux wcfs-server 6.8.0-124-generic #124-Ubuntu SMP PREEMPT_DYNAMIC Tue May 26 13:00:45 UTC 2026 x86_64 User : nodor ( 1000) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/wcfs/tabs/ |
Upload File : |
<?php
session_start();
include 'protect.php';
include 'db.php';
include 'store_settings.php';
require_once __DIR__ . '/brand_header.php';
require_once __DIR__ . '/qbo_name_map_helpers.php';
require_once __DIR__ . '/export_helpers.php';
function eh($value)
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function exportReviewCategoryAnchor($category)
{
$safe = preg_replace('/[^A-Za-z0-9_-]+/', '-', (string) $category);
$safe = trim((string) $safe, '-');
return $safe === '' ? 'category' : $safe;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['prepare_export_review'])) {
$month = isset($_POST['month']) ? trim((string) $_POST['month']) : '';
$exportScope = isset($_POST['export_scope']) ? trim((string) $_POST['export_scope']) : 'month';
$startDate = isset($_POST['start_date']) ? trim((string) $_POST['start_date']) : '';
$endDate = isset($_POST['end_date']) ? trim((string) $_POST['end_date']) : '';
$includeArchived = !empty($_POST['include_archived']);
$mapQboNames = !empty($_POST['map_qbo_names']);
if (!in_array($exportScope, array('month', 'range'), true)) {
$exportScope = 'month';
}
if ($exportScope === 'month' && !preg_match('/^\d{4}-\d{2}$/', $month)) {
http_response_code(400);
echo 'Please choose a valid export month.';
exit;
}
if ($exportScope === 'range') {
$startDate = normalizePrivateExportDate($startDate);
$endDate = normalizePrivateExportDate($endDate);
if ($startDate === '' || $endDate === '') {
http_response_code(400);
echo 'Please choose a valid export date range.';
exit;
}
if (strtotime($startDate) > strtotime($endDate)) {
http_response_code(400);
echo 'The export start date must be before the end date.';
exit;
}
$month = substr($startDate, 0, 7);
} else {
$startDate = '';
$endDate = '';
}
if ($mapQboNames) {
try {
$mappingFile = isset($_FILES['mapping_file']) ? $_FILES['mapping_file'] : array();
$fileError = isset($mappingFile['error']) ? (int) $mappingFile['error'] : UPLOAD_ERR_NO_FILE;
if ($fileError === UPLOAD_ERR_NO_FILE) {
qboNameMapLoadSaved($conn);
} else {
qboNameMapStoreUploadedFile($conn, $mappingFile);
}
} catch (Exception $e) {
http_response_code(400);
echo htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
exit;
}
}
$_SESSION['private_export_review'] = array(
'export_scope' => $exportScope,
'month' => $month,
'start_date' => $startDate,
'end_date' => $endDate,
'range_label' => privateExportRangeLabel($month, $startDate, $endDate),
'include_archived' => $includeArchived ? 1 : 0,
'map_qbo_names' => $mapQboNames ? 1 : 0,
'rows' => privatePreparedExportRows($conn, $month, $includeArchived, $mapQboNames, array(), $startDate, $endDate),
);
setAppSettingValue($conn, 'private_qbo_last_export_range', $_SESSION['private_export_review']['range_label']);
$_SESSION['private_export_review_overrides'] = array();
header('Location: export_review.php');
exit;
}
$review = isset($_SESSION['private_export_review']) ? $_SESSION['private_export_review'] : null;
if (!$review || empty($review['rows']) || !is_array($review['rows'])) {
header('Location: export_form.php');
exit;
}
$overrideMap = isset($_SESSION['private_export_review_overrides']) && is_array($_SESSION['private_export_review_overrides'])
? $_SESSION['private_export_review_overrides']
: array();
$openSections = isset($_SESSION['private_export_review_open_sections']) && is_array($_SESSION['private_export_review_open_sections'])
? $_SESSION['private_export_review_open_sections']
: array('Misc');
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_category_override'])) {
$overrideItem = isset($_POST['override_item']) ? strtolower(trim((string) $_POST['override_item'])) : '';
$overrideCategory = isset($_POST['override_category']) ? trim((string) $_POST['override_category']) : '';
$fromCategory = isset($_POST['from_category']) ? trim((string) $_POST['from_category']) : '';
$postedOpenSections = isset($_POST['open_sections']) ? trim((string) $_POST['open_sections']) : '';
if ($postedOpenSections !== '') {
$decodedOpen = json_decode($postedOpenSections, true);
if (is_array($decodedOpen)) {
$openSections = array_values(array_unique(array_map('strval', $decodedOpen)));
}
}
if ($overrideItem !== '' && $overrideCategory !== '') {
$overrideMap[$overrideItem] = $overrideCategory;
$_SESSION['private_export_review_overrides'] = $overrideMap;
if ($fromCategory !== '' && !in_array($fromCategory, $openSections, true)) {
$openSections[] = $fromCategory;
}
if (!in_array($overrideCategory, $openSections, true)) {
$openSections[] = $overrideCategory;
}
$_SESSION['private_export_review_open_sections'] = $openSections;
$autosaveOk = false;
$mapFile = privateCategorizerDir() . '/item_map.json';
$itemMap = privateCategorizerMap();
$itemMap[$overrideItem] = $overrideCategory;
if (@file_put_contents($mapFile, json_encode($itemMap, JSON_PRETTY_PRINT))) {
$autosaveOk = true;
}
$_SESSION['private_export_review_flash'] = $autosaveOk
? array('type' => 'ok', 'message' => 'Category saved.')
: array('type' => 'warn', 'message' => 'Category used for this export, but could not save to your map file.');
}
$returnAnchor = $fromCategory !== '' ? exportReviewCategoryAnchor($fromCategory) : exportReviewCategoryAnchor($overrideCategory);
header('Location: export_review.php#' . $returnAnchor);
exit;
}
$month = isset($review['month']) ? (string) $review['month'] : date('Y-m');
$startDate = isset($review['start_date']) ? (string) $review['start_date'] : '';
$endDate = isset($review['end_date']) ? (string) $review['end_date'] : '';
$rangeLabel = isset($review['range_label']) ? (string) $review['range_label'] : privateExportRangeLabel($month, $startDate, $endDate);
$rules = privateCategorizerRules();
$baseItemMap = privateCategorizerMap();
$effectiveItemMap = privateMergedCategorizerMap($overrideMap);
$categories = array_keys($rules);
if (!in_array('Misc', $categories, true)) {
$categories[] = 'Misc';
}
$categorizerDir = privateCategorizerDir();
$rows = privatePreparedExportRows(
$conn,
(string) $review['month'],
!empty($review['include_archived']),
!empty($review['map_qbo_names']),
$overrideMap,
$startDate,
$endDate
);
$groupedRows = array();
foreach ($categories as $categoryName) {
$groupedRows[$categoryName] = array();
}
foreach ($rows as $row) {
$category = isset($row['Category']) ? (string) $row['Category'] : 'Misc';
if (!isset($groupedRows[$category])) {
$groupedRows[$category] = array();
}
$groupedRows[$category][] = $row;
}
$storeName = 'HouseTabPro';
if (isset($_SESSION['store_name']) && $_SESSION['store_name'] !== '') {
$storeName = $_SESSION['store_name'];
} elseif (isset($_SESSION['store_db']) && $_SESSION['store_db'] !== '') {
$storeName = $_SESSION['store_db'];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['download_reviewed_export'])) {
$preparedRows = privatePreparedExportRows(
$conn,
(string) $review['month'],
!empty($review['include_archived']),
!empty($review['map_qbo_names']),
$overrideMap,
$startDate,
$endDate
);
$output = fopen('php://temp', 'r+');
fputcsv($output, array('Date', 'Customer', 'Item', 'Category', 'Quantity', 'Price', 'Subtotal', 'Taxable', 'Tax', 'Total', 'Notes', 'Source'));
foreach ($preparedRows as $row) {
fputcsv($output, array(
$row['Date'],
$row['Customer'],
$row['Item'],
$row['Category'],
$row['Quantity'],
$row['Price'],
$row['Subtotal'],
$row['Taxable'],
$row['Tax'],
$row['Total'],
$row['Notes'],
$row['Source'],
));
}
rewind($output);
$csv = stream_get_contents($output);
fclose($output);
header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="' . exportFilenameForStore($storeName, $month, !empty($review['map_qbo_names']), $startDate, $endDate) . '"');
echo $csv;
unset($_SESSION['private_export_review_overrides']);
unset($_SESSION['private_export_review_open_sections']);
exit;
}
$flash = isset($_SESSION['private_export_review_flash']) ? $_SESSION['private_export_review_flash'] : null;
unset($_SESSION['private_export_review_flash']);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Review Export Categories</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 30px;
background: #f8fafc;
color: #1f2937;
}
.container {
max-width: 980px;
margin: 0 auto;
background: #fff;
padding: 28px;
border-radius: 16px;
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.08);
}
.toolbar {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 18px;
}
.btn {
display: inline-block;
padding: 10px 14px;
background: #2563eb;
color: #fff;
text-decoration: none;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
}
.btn-secondary {
background: #64748b;
}
.msg {
margin: 14px 0;
padding: 12px 14px;
border-radius: 10px;
}
.msg.ok {
background: #ecfdf5;
border: 1px solid #86efac;
color: #166534;
}
.msg.warn {
background: #fff7ed;
border: 1px solid #fdba74;
color: #9a3412;
}
.category-header {
background: #1e293b;
color: white;
padding: 10px 12px;
cursor: pointer;
margin-top: 14px;
border-radius: 8px;
font-weight: 700;
scroll-margin-top: 18px;
}
.category-content {
display: none;
border: 1px solid #cbd5e1;
border-radius: 8px;
padding: 10px;
margin-bottom: 10px;
background: #f8fafc;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th, td {
padding: 7px;
border: 1px solid #cbd5e1;
text-align: left;
vertical-align: top;
}
select {
width: 100%;
padding: 7px 8px;
}
.searchBox {
margin: 10px 0 14px;
padding: 8px 10px;
width: 280px;
max-width: 100%;
}
.muted {
color: #64748b;
font-size: 0.92rem;
}
</style>
<?php echo appBrandStyles(); ?>
</head>
<body>
<div class="container">
<?php renderAppBrandHeader(); ?>
<div class="toolbar">
<div>
<h1 style="margin:0;">Review Export Categories</h1>
<div class="muted" style="margin-top:6px;">
<?php echo eh($rangeLabel); ?> export prepared. Notes have already been merged into the item description, and known items have already been categorized.
</div>
<div class="muted" style="margin-top:6px;">
Using categorizer folder: <strong><?php echo eh($categorizerDir); ?></strong>
</div>
</div>
<div>
<a class="btn btn-secondary" href="export_form.php">Back to Export</a>
</div>
</div>
<?php if (is_array($flash) && !empty($flash['message'])): ?>
<div class="msg <?php echo ($flash['type'] === 'ok') ? 'ok' : 'warn'; ?>">
<?php echo eh($flash['message']); ?>
</div>
<?php endif; ?>
<input type="text" id="searchBox" class="searchBox" placeholder="Search item descriptions..." onkeyup="filterItems()">
<button type="button" class="btn btn-secondary" onclick="resetSearch()">Reset</button>
<div id="categoriesContainer">
<?php foreach ($categories as $categoryName): ?>
<div id="<?php echo eh(exportReviewCategoryAnchor($categoryName)); ?>" class="category-header" onclick="toggleCategory(this)"><?php echo eh($categoryName); ?> (<?php echo count(isset($groupedRows[$categoryName]) ? $groupedRows[$categoryName] : array()); ?>)</div>
<div class="category-content" style="display:<?php echo in_array($categoryName, $openSections, true) ? 'block' : 'none'; ?>;">
<table>
<tr>
<th>Item</th>
<th>Customer</th>
<th>Category</th>
<th>Total</th>
</tr>
<?php foreach (isset($groupedRows[$categoryName]) ? $groupedRows[$categoryName] : array() as $row): ?>
<tr>
<td><?php echo eh(isset($row['Item']) ? $row['Item'] : ''); ?></td>
<td><?php echo eh(isset($row['Customer']) ? $row['Customer'] : ''); ?></td>
<td>
<form method="post" style="margin:0;">
<input type="hidden" name="save_category_override" value="1">
<input type="hidden" name="override_item" value="<?php echo eh(strtolower(isset($row['BaseItem']) ? $row['BaseItem'] : (isset($row['Item']) ? $row['Item'] : ''))); ?>">
<input type="hidden" name="from_category" value="<?php echo eh($categoryName); ?>">
<input type="hidden" name="open_sections" class="open-sections-field" value="">
<select name="override_category" onchange="this.form.submit()">
<?php foreach ($categories as $option): ?>
<option value="<?php echo eh($option); ?>" <?php echo ((isset($row['Category']) ? $row['Category'] : 'Misc') === $option) ? 'selected' : ''; ?>>
<?php echo eh($option); ?>
</option>
<?php endforeach; ?>
</select>
</form>
</td>
<td><?php echo eh(isset($row['Total']) ? $row['Total'] : ''); ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
<?php endforeach; ?>
</div>
<form method="post" style="margin-top:18px;">
<input type="hidden" name="download_reviewed_export" value="1">
<button type="submit" class="btn">Download Final CSV</button>
</form>
</div>
<script>
function currentOpenSections() {
var open = [];
var headers = document.querySelectorAll('.category-header');
headers.forEach(function (header) {
var content = header.nextElementSibling;
if (content && content.style.display === 'block') {
var label = header.textContent || '';
var match = label.match(/^(.*?)\s+\(\d+\)$/);
open.push(match ? match[1] : label);
}
});
return open;
}
function toggleCategory(header) {
var content = header.nextElementSibling;
if (!content) return;
content.style.display = content.style.display === 'block' ? 'none' : 'block';
}
function filterItems() {
var term = document.getElementById('searchBox').value.toLowerCase();
var tables = document.querySelectorAll('.category-content table');
tables.forEach(function (table) {
var rows = table.querySelectorAll('tr');
rows.forEach(function (row, index) {
if (index === 0) return;
var itemText = row.cells[0].innerText.toLowerCase();
row.style.display = itemText.indexOf(term) !== -1 ? '' : 'none';
});
});
}
function resetSearch() {
document.getElementById('searchBox').value = '';
filterItems();
}
document.querySelectorAll('form').forEach(function (form) {
form.addEventListener('submit', function () {
var field = form.querySelector('.open-sections-field');
if (field) {
field.value = JSON.stringify(currentOpenSections());
}
});
});
</script>
</body>
</html>