| 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/categorize/ |
Upload File : |
<?php
session_start();
/* File paths */
$rules_file = __DIR__ . "/category_rules.json";
$map_file = __DIR__ . "/item_map.json";
$uploads = __DIR__ . "/uploads/";
$output = __DIR__ . "/output/";
/* Ensure directories exist */
if (!is_dir($uploads)) mkdir($uploads, 0775, true);
if (!is_dir($output)) mkdir($output, 0775, true);
/* Load rules */
$rules = json_decode(@file_get_contents($rules_file), true);
if (!$rules || !is_array($rules)) {
$rules = [
"No Tax Grocery" => [],
"Grocery (Taxable)" => [],
"Hardware" => [],
"Feed" => [],
"Oil" => [],
"Unleaded" => [],
"Diesel" => [],
"Off Road Diesel" => [],
"Misc" => []
];
}
/* Load Item Map (lowercased item name => category) */
$item_map = json_decode(@file_get_contents($map_file), true);
if (!$item_map || !is_array($item_map)) {
$item_map = [];
}
/* Categorize function (PHP side) */
function categorize_item($item, $rules, $map) {
$lower = strtolower(trim($item));
// 1. exact match mapping from item_map.json
if (isset($map[$lower])) {
return $map[$lower];
}
// 2. rule keyword match
foreach ($rules as $category => $keywords) {
foreach ($keywords as $kw) {
$kw = trim($kw);
if ($kw !== "" && strpos($lower, strtolower($kw)) !== false) {
return $category;
}
}
}
// 3. fallback
return "Misc";
}
/* Upload handling */
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_FILES["csvfile"])) {
$filename = basename($_FILES["csvfile"]["name"]);
$target = $uploads . $filename;
if (move_uploaded_file($_FILES["csvfile"]["tmp_name"], $target)) {
$rows = array_map("str_getcsv", file($target));
$header = array_shift($rows);
$_SESSION["rows"] = $rows;
$_SESSION["header"] = $header;
$_SESSION["file"] = $target;
} else {
echo "<div style='color:red;font-weight:bold'>Error uploading file.</div>";
}
}
$rows = $_SESSION["rows"] ?? null;
$header = $_SESSION["header"] ?? null;
/* Export CSV */
if (isset($_POST["export_csv"])) {
// ?? Reload the latest map from disk so all your recent selections are used
$item_map = json_decode(@file_get_contents($map_file), true);
if (!$item_map || !is_array($item_map)) {
$item_map = [];
}
$rows = $_SESSION['rows'];
$header = $_SESSION['header'];
// Add category column
$header[] = "Category";
$filename = "categorized_" . date("Y-m-d_Hi") . ".csv";
$filepath = $output . $filename;
$fp = fopen($filepath, "w");
fputcsv($fp, $header);
foreach ($rows as $r) {
$item = $r[3]; // item column
$cat = categorize_item($item, $rules, $item_map);
$r[] = $cat;
fputcsv($fp, $r);
}
fclose($fp);
$export_done = $filename;
}
?>
<!DOCTYPE html>
<html>
<head>
<title>CSV Categorizer</title>
<style>
body {
font-family: Arial, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 950px;
background: white;
padding: 20px;
margin: auto;
border-radius: 10px;
box-shadow: 0 0 10px #ccc;
}
.category-header {
background: #333;
color: white;
padding: 10px;
cursor: pointer;
margin-top: 15px;
border-radius: 5px;
}
.category-content {
display: none;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
}
/* Row category colors */
.cat-NoTaxGrocery { background: #e8ffe8; }
.cat-GroceryTaxable { background: #fff3d6; }
.cat-Hardware { background: #e7f0ff; }
.cat-Feed { background: #fffbe0; }
.cat-Oil { background: #f3e8ff; }
.cat-Unleaded { background: #ffe6ef; }
.cat-Diesel { background: #e6fffb; }
.cat-OffRoadDiesel { background: #f2f2f2; }
.cat-Misc { background: #ffffff; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { padding: 6px; border: 1px solid #ccc; }
.searchBox {
margin: 15px 0;
padding: 6px;
width: 250px;
}
.success {
background:#d9ffd9;
padding:10px;
margin-top:10px;
border-radius:6px;
}
#statusBox {
margin-top: 10px;
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="container">
<h2>Copy item before combine Upload CSV</h2>
<form method="post" enctype="multipart/form-data">
<input type="file" name="csvfile" required>
<button type="submit">Upload & Process</button>
</form>
<?php if (!empty($export_done)): ?>
<div class="success">
Export complete!
<a href="output/<?= htmlspecialchars($export_done) ?>" download>
Download <?= htmlspecialchars($export_done) ?>
</a>
</div>
<?php endif; ?>
<?php if ($rows): ?>
<hr>
<h2>Preview & Categorize</h2>
<input type="text" id="searchBox" class="searchBox"
placeholder="Search..." onkeyup="filterItems()">
<button type="button" onclick="resetSearch()">Reset</button>
<div id="statusBox"></div>
<div id="categoriesContainer"></div>
<form method="post">
<input type="hidden" name="export_csv" value="1">
<button type="submit">Export Categorized CSV</button>
</form>
</div>
<script>
let rules = <?php echo json_encode($rules); ?>;
let item_map = <?php echo json_encode($item_map); ?>; // lowercased item => category
let rows = <?php echo json_encode($rows); ?>;
let categories = Object.keys(rules);
let openSections = {}; // track which sections are open
/*** Build expandable category UI ***/
function buildUI() {
let container = document.getElementById("categoriesContainer");
container.innerHTML = "";
let grouped = {};
// Group rows by category (using current JS-side item_map + rules)
rows.forEach((r, idx) => {
let item = r[3];
let lower = item.toLowerCase();
let cat = item_map[lower] ?? categorizeByRules(item);
if (!grouped[cat]) grouped[cat] = [];
grouped[cat].push({ row: r, index: idx });
});
categories.forEach(cat => {
let safe = cat.replace(/[^A-Za-z0-9]/g,"");
let header = document.createElement("div");
header.className = "category-header";
header.innerText = cat;
let content = document.createElement("div");
content.className = "category-content";
// Restore open/closed state
content.style.display = openSections[cat] ? "block" : "none";
header.onclick = () => {
openSections[cat] = !openSections[cat];
content.style.display = openSections[cat] ? "block" : "none";
};
let html = "<table><tr><th>Item</th><th>Category</th></tr>";
(grouped[cat] || []).forEach(obj => {
let item = obj.row[3];
let lower = item.toLowerCase();
let category = item_map[lower] ?? categorizeByRules(item);
let safeClass = "cat-" + category.replace(/[^A-Za-z0-9]/g, "");
html += "<tr class='"+safeClass+"'><td>"+item+"</td>" +
"<td><select onchange=\"changeCategory('"+lower+"', this.value)\">" +
categories.map(c => "<option "+(c===category?"selected":"")+">"+c+"</option>").join("") +
"</select></td></tr>";
});
html += "</table>";
content.innerHTML = html;
container.appendChild(header);
container.appendChild(content);
});
}
/*** Categorize using rules (JS side, for preview only) ***/
function categorizeByRules(item) {
let lower = item.toLowerCase();
for (let cat in rules) {
for (let kw of rules[cat]) {
if (kw && lower.includes(kw.toLowerCase())) {
return cat;
}
}
}
return "Misc";
}
/*** Auto-save category mapping + keep sections open ***/
function changeCategory(itemLower, newCat) {
let statusBox = document.getElementById("statusBox");
if (statusBox) {
statusBox.style.color = "#333";
statusBox.textContent = "Saving...";
}
fetch("autosave.php", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "item=" + encodeURIComponent(itemLower) +
"&category=" + encodeURIComponent(newCat)
})
.then(r => r.text())
.then(txt => {
txt = (txt || "").trim();
if (txt !== "OK") {
if (statusBox) {
statusBox.style.color = "red";
statusBox.textContent = "Error saving mapping: " + txt;
}
return;
}
// Update in JS memory
item_map[itemLower] = newCat;
// Keep new category open
openSections[newCat] = true;
if (statusBox) {
statusBox.style.color = "green";
statusBox.textContent = "Saved.";
}
// Rebuild UI, but preserve openSections state
buildUI();
})
.catch(err => {
if (statusBox) {
statusBox.style.color = "red";
statusBox.textContent = "Save failed: " + err;
}
});
}
/*** Search filter */
function filterItems() {
let term = document.getElementById("searchBox").value.toLowerCase();
let tables = document.querySelectorAll(".category-content table");
tables.forEach(tbl => {
let rows = tbl.querySelectorAll("tr");
rows.forEach((r, i) => {
if (i === 0) return;
let txt = r.cells[0].innerText.toLowerCase();
r.style.display = txt.includes(term) ? "" : "none";
});
});
}
function resetSearch() {
document.getElementById("searchBox").value = "";
filterItems();
}
buildUI();
</script>
<?php endif; ?>
</body>
</html>