| 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/thelittlebigshow/self-hosted/ |
Upload File : |
<?php
include 'protect.php';
include 'db.php';
include 'store_settings.php';
require_once __DIR__ . '/qbo_name_map_helpers.php';
require_once __DIR__ . '/scheduled_export_helpers.php';
require_once __DIR__ . '/brand_header.php';
require_once __DIR__ . '/../stripe_checkout_helpers.php';
$selectedMonth = trim((string) ($_GET['month'] ?? date('Y-m')));
$months = [];
$mappingHelp = 'Upload a CSV with the HouseTab Pro customer name in the first column and the matching QuickBooks name in the second column.';
$savedMap = qboNameMapGetMeta($conn);
$scheduleMessage = '';
$scheduleError = '';
$defaultEmail = trim((string) ($_SESSION['account_email'] ?? ''));
$scheduleSettings = scheduledExportSettings($conn, $defaultEmail);
$scheduleAddonEnabled = !empty($scheduleSettings['addon_enabled']);
$centralAddonActive = false;
$isImpersonatingStore = !empty($_SESSION['is_impersonating_store']);
$backupFlash = $_SESSION['export_backup_flash'] ?? null;
unset($_SESSION['export_backup_flash']);
if (!empty($_SESSION['account_id'])) {
try {
$centralDb = stripeCentralDb();
$addonSummary = getStripeAddonSummary($centralDb, (int) $_SESSION['account_id'], 'scheduled_export');
$addonStatus = strtolower(trim((string) ($addonSummary['billing_status'] ?? '')));
if ($addonSummary && !in_array($addonStatus, ['canceled', 'cancelled', 'unpaid', 'incomplete_expired'], true)) {
$centralAddonActive = true;
if (!$scheduleAddonEnabled) {
setAppSettingValue($conn, 'scheduled_export_addon_enabled', '1');
$scheduleSettings = scheduledExportSettings($conn, $defaultEmail);
}
}
} catch (Throwable $e) {
// Keep the export page usable even if the billing self-heal check cannot run.
}
}
$scheduleAddonEnabled = $scheduleAddonEnabled || $centralAddonActive;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string) ($_POST['action'] ?? '') === 'save_schedule') {
$scheduleSettings = [
'addon_enabled' => $scheduleSettings['addon_enabled'] ?? false,
'enabled' => !empty($_POST['schedule_enabled']),
'email' => trim((string) ($_POST['schedule_email'] ?? '')),
'times_per_day' => (int) ($_POST['schedule_times_per_day'] ?? 0),
'times' => array_values((array) ($_POST['schedule_times'] ?? [])),
'include_archived' => !empty($_POST['schedule_include_archived']),
'map_qbo_names' => !empty($_POST['schedule_map_qbo_names']),
'last_sent_at' => $scheduleSettings['last_sent_at'] ?? '',
'last_sent_marker' => $scheduleSettings['last_sent_marker'] ?? '',
];
try {
if (!$scheduleAddonEnabled) {
throw new RuntimeException('Scheduled email exports are available as a paid add-on.');
}
if ($scheduleSettings['enabled']) {
if (!filter_var($scheduleSettings['email'], FILTER_VALIDATE_EMAIL)) {
throw new RuntimeException('Please enter a valid email address for scheduled exports.');
}
if ($scheduleSettings['times_per_day'] < 1 || $scheduleSettings['times_per_day'] > 5) {
throw new RuntimeException('Please choose a send frequency between 1 and 5 times per day.');
}
if ($scheduleSettings['map_qbo_names'] && empty($savedMap['exists']) && (($_FILES['mapping_file']['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE)) {
throw new RuntimeException('Upload a QBO mapping file before turning on mapped scheduled exports.');
}
}
if (($_FILES['mapping_file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) {
qboNameMapStoreUploadedFile($conn, $_FILES['mapping_file']);
$savedMap = qboNameMapGetMeta($conn);
}
saveScheduledExportSettings($conn, $scheduleSettings);
$scheduleSettings = scheduledExportSettings($conn, $defaultEmail);
$scheduleMessage = $scheduleSettings['enabled']
? 'Scheduled exports saved. We will email this export at ' . scheduledExportReadableTimes((array) ($scheduleSettings['times'] ?? [])) . '.'
: 'Scheduled exports are now turned off.';
} catch (Throwable $e) {
$scheduleError = $e->getMessage();
}
}
for ($i = 0; $i < 24; $i++) {
$months[] = date('Y-m', strtotime("-{$i} months"));
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Export to CSV</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 30px;
background: #f8fafc;
color: #1f2937;
}
.container {
max-width: 760px;
margin: 0 auto;
background: #fff;
padding: 28px;
border-radius: 16px;
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.08);
}
.card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 20px;
margin-top: 18px;
}
h1 {
margin-top: 0;
}
p {
color: #475569;
line-height: 1.6;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 700;
}
select,
input[type="file"],
input[type="email"] {
width: 100%;
max-width: 320px;
padding: 12px 14px;
border: 1px solid #cbd5e1;
border-radius: 10px;
box-sizing: border-box;
font-size: 15px;
}
.checkbox-row {
margin-top: 16px;
color: #334155;
}
.checkbox-row label {
display: inline-flex;
align-items: center;
gap: 10px;
font-weight: 500;
margin-bottom: 0;
}
.helper-text {
margin-top: 10px;
font-size: 14px;
color: #64748b;
line-height: 1.5;
}
.saved-map {
margin-top: 12px;
padding: 12px 14px;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 10px;
color: #1d4ed8;
font-size: 14px;
}
.msg {
margin-top: 16px;
padding: 12px 14px;
border-radius: 10px;
}
.msg.ok {
background: #ecfdf5;
border: 1px solid #86efac;
color: #166534;
}
.msg.err {
background: #fef2f2;
border: 1px solid #fca5a5;
color: #991b1b;
}
.msg.warn {
background: #fff7ed;
border: 1px solid #fdba74;
color: #9a3412;
}
.two-col {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px 22px;
}
.badge {
display: inline-block;
margin-left: 10px;
padding: 5px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.03em;
background: #dbeafe;
color: #1d4ed8;
}
.addon-lock {
background: #fff7ed;
border: 1px solid #fdba74;
color: #9a3412;
}
.addon-lock strong {
color: #7c2d12;
}
.preview-disabled {
margin-top: 16px;
padding: 14px;
border: 1px dashed #cbd5e1;
border-radius: 10px;
background: #f8fafc;
opacity: 0.62;
}
.preview-disabled .preview-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px 18px;
margin-top: 12px;
}
.preview-disabled label {
font-weight: 700;
}
.preview-disabled input[type="email"],
.preview-disabled select,
.preview-disabled input[type="time"] {
width: 100%;
max-width: 320px;
padding: 12px 14px;
border: 1px solid #cbd5e1;
border-radius: 10px;
box-sizing: border-box;
font-size: 15px;
background: #e2e8f0;
color: #475569;
}
.cta-danger {
background: #b91c1c;
}
.cta-danger:hover {
background: #991b1b;
}
.times-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px 18px;
margin-top: 16px;
}
.time-input-wrap {
display: flex;
flex-direction: column;
}
.time-input-wrap input[type="time"] {
width: 100%;
max-width: 220px;
padding: 12px 14px;
border: 1px solid #cbd5e1;
border-radius: 10px;
box-sizing: border-box;
font-size: 15px;
}
.btn {
display: inline-block;
margin-top: 20px;
padding: 12px 18px;
background: #2563eb;
color: #fff;
text-decoration: none;
border: none;
border-radius: 10px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
}
.btn:hover {
background: #1d4ed8;
}
.back-link {
display: inline-block;
margin-top: 18px;
color: #2563eb;
text-decoration: none;
}
@media (max-width: 700px) {
.two-col {
grid-template-columns: 1fr;
}
.preview-disabled .preview-grid {
grid-template-columns: 1fr;
}
.times-grid {
grid-template-columns: 1fr;
}
}
</style>
<?php echo appBrandStyles(); ?>
</head>
<body>
<div class="container">
<?php renderAppBrandHeader(); ?>
<h1>Export Transactions to CSV</h1>
<p>Choose the month you want to export. The CSV will include matching live and archived transactions for that month so you can review or import them elsewhere.</p>
<form method="post" action="export_csv.php" enctype="multipart/form-data" class="card">
<label for="month">Month to Export</label>
<select name="month" id="month" required>
<?php foreach ($months as $month): ?>
<option value="<?php echo htmlspecialchars($month, ENT_QUOTES, 'UTF-8'); ?>" <?php echo $month === $selectedMonth ? 'selected' : ''; ?>>
<?php echo htmlspecialchars(date('F Y', strtotime($month . '-01')), ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<div class="checkbox-row">
<label>
<input type="checkbox" name="include_archived" value="1" checked>
Include archived transactions from that month too
</label>
</div>
<div class="checkbox-row">
<label>
<input type="checkbox" name="map_qbo_names" value="1">
Map to QBO customer names
</label>
</div>
<div style="margin-top:16px;">
<label for="mapping_file">QBO Name Mapping File</label>
<input type="file" id="mapping_file" name="mapping_file" accept=".csv,text/csv">
<div class="helper-text">
<?php echo htmlspecialchars($mappingHelp, ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php if (!empty($savedMap['exists'])): ?>
<div class="saved-map">
Saved file:
<strong><?php echo htmlspecialchars($savedMap['original_name'] !== '' ? $savedMap['original_name'] : 'Saved QBO mapping file', ENT_QUOTES, 'UTF-8'); ?></strong>
<?php if (!empty($savedMap['saved_at']) && strtotime((string) $savedMap['saved_at']) !== false): ?>
<br>Saved on <?php echo htmlspecialchars(date('F j, Y g:i A', strtotime((string) $savedMap['saved_at'])), ENT_QUOTES, 'UTF-8'); ?>
<?php endif; ?>
<br>Leave the upload box empty next time if you want to reuse this saved mapping.
</div>
<?php endif; ?>
</div>
<button type="submit" class="btn">Download CSV</button>
<a href="download_database_backup.php" class="btn" style="margin-left:10px;background:#0f766e;">Download Database Backup</a>
</form>
<form method="post" action="restore_database_backup.php" enctype="multipart/form-data" class="card">
<h2 style="margin-top:0;">Restore Database Backup</h2>
<p>Upload a HouseTab Pro SQL backup for this store to restore its data. This only restores the current store database and cannot be used across stores.</p>
<?php if (is_array($backupFlash) && !empty($backupFlash['message'])): ?>
<div class="msg <?php echo ($backupFlash['type'] ?? '') === 'ok' ? 'ok' : 'err'; ?>">
<?php echo htmlspecialchars((string) $backupFlash['message'], ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php endif; ?>
<div class="msg warn">
This will overwrite the current store database. A temporary safety snapshot is created first, and the app will try to roll back automatically if the uploaded backup fails.
</div>
<div style="margin-top:16px;">
<label for="restore_backup_file">Backup File</label>
<input type="file" id="restore_backup_file" name="restore_backup_file" accept=".sql,.gz,.sql.gz,text/plain,application/gzip" required>
<div class="helper-text">Use a backup created from HouseTab Pro, usually a <code>.sql</code> or <code>.sql.gz</code> file.</div>
</div>
<div style="margin-top:16px;">
<label for="confirm_restore_text">Type RESTORE to Confirm</label>
<input type="text" id="confirm_restore_text" name="confirm_restore_text" placeholder="RESTORE" style="width:100%;max-width:320px;padding:12px 14px;border:1px solid #cbd5e1;border-radius:10px;box-sizing:border-box;font-size:15px;" required>
</div>
<button type="submit" class="btn" style="background:#b91c1c;" onclick="return confirm('Restore this store database from the uploaded backup? This will overwrite the current data.');">Restore Database Backup</button>
</form>
<form method="post" enctype="multipart/form-data" class="card">
<input type="hidden" name="action" value="save_schedule">
<h2 style="margin-top:0;">Scheduled Email Exports <span class="badge">Paid Add-On</span></h2>
<p>Manual CSV export stays available any time. Scheduled email exports are a separate paid add-on for stores that want automatic deliveries.</p>
<?php if ($scheduleMessage !== ''): ?>
<div class="msg ok"><?php echo htmlspecialchars($scheduleMessage, ENT_QUOTES, 'UTF-8'); ?></div>
<?php endif; ?>
<?php if ($scheduleError !== ''): ?>
<div class="msg err"><?php echo htmlspecialchars($scheduleError, ENT_QUOTES, 'UTF-8'); ?></div>
<?php endif; ?>
<?php if (!$scheduleAddonEnabled): ?>
<div class="saved-map addon-lock">
<strong>Scheduled email exports are available as a paid add-on.</strong>
<br>Add this feature for automatic emailed CSV exports up to 5 times per day.
<br><strong>$7/month</strong> per hosted store.
<div class="preview-disabled">
<div style="font-weight:700;color:#475569;">Preview of scheduled export settings</div>
<div class="preview-grid">
<div>
<label>Send To Email</label>
<input type="email" value="<?php echo htmlspecialchars((string) ($scheduleSettings['email'] ?: $defaultEmail), ENT_QUOTES, 'UTF-8'); ?>" disabled>
</div>
<div>
<label>How Many Times Per Day</label>
<select disabled>
<option selected>Choose up to 5 send times</option>
</select>
</div>
</div>
<div class="preview-grid">
<div>
<label>Send Time 1</label>
<input type="time" value="09:00" disabled>
</div>
<div>
<label>Send Time 2</label>
<input type="time" value="14:00" disabled>
</div>
</div>
<div class="checkbox-row">
<label><input type="checkbox" checked disabled> Include archived transactions in scheduled exports</label>
</div>
<div class="checkbox-row">
<label><input type="checkbox" disabled> Apply saved QBO name mapping before emailing</label>
</div>
</div>
<div style="margin-top:14px;">
<?php if ($isImpersonatingStore): ?>
<span class="btn cta-danger" style="opacity:.92;cursor:not-allowed;">Paid Add-On - $7/month</span>
<?php else: ?>
<a class="btn cta-danger" href="start_scheduled_export_addon_checkout.php">Paid Add-On - $7/month</a>
<?php endif; ?>
</div>
</div>
<?php else: ?>
<div class="msg ok" style="margin-top:16px;">
Scheduled email exports are active for this store.
</div>
<div class="checkbox-row">
<label>
<input type="checkbox" name="schedule_enabled" value="1" <?php echo !empty($scheduleSettings['enabled']) ? 'checked' : ''; ?>>
Turn on scheduled export emails
</label>
</div>
<div class="two-col" style="margin-top:16px;">
<div>
<label for="schedule_email">Send To Email</label>
<input type="email" id="schedule_email" name="schedule_email" value="<?php echo htmlspecialchars((string) ($scheduleSettings['email'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>" placeholder="you@example.com">
</div>
<div>
<label for="schedule_times_per_day">How Many Times Per Day</label>
<select name="schedule_times_per_day" id="schedule_times_per_day">
<option value="0" <?php echo (int) ($scheduleSettings['times_per_day'] ?? 0) === 0 ? 'selected' : ''; ?>>Do not send</option>
<?php for ($i = 1; $i <= 5; $i++): ?>
<option value="<?php echo $i; ?>" <?php echo (int) ($scheduleSettings['times_per_day'] ?? 0) === $i ? 'selected' : ''; ?>>
<?php echo $i; ?> time<?php echo $i === 1 ? '' : 's'; ?> per day
</option>
<?php endfor; ?>
</select>
</div>
</div>
<div class="times-grid">
<?php
$savedTimes = array_values((array) ($scheduleSettings['times'] ?? []));
for ($i = 0; $i < 5; $i++):
$timeValue = $savedTimes[$i] ?? (scheduledExportDefaultTimes($i + 1)[$i] ?? '');
?>
<div class="time-input-wrap">
<label for="schedule_time_<?php echo $i; ?>">Send Time <?php echo $i + 1; ?></label>
<input
type="time"
id="schedule_time_<?php echo $i; ?>"
name="schedule_times[]"
value="<?php echo htmlspecialchars($timeValue, ENT_QUOTES, 'UTF-8'); ?>"
>
</div>
<?php endfor; ?>
</div>
<div class="helper-text">Choose the times you want these export emails to go out. Only the first selected number of times per day will be used.</div>
<div class="checkbox-row">
<label>
<input type="checkbox" name="schedule_include_archived" value="1" <?php echo !empty($scheduleSettings['include_archived']) ? 'checked' : ''; ?>>
Include archived transactions in scheduled exports
</label>
</div>
<div class="checkbox-row">
<label>
<input type="checkbox" name="schedule_map_qbo_names" value="1" <?php echo !empty($scheduleSettings['map_qbo_names']) ? 'checked' : ''; ?>>
Apply saved QBO name mapping before emailing
</label>
</div>
<div style="margin-top:16px;">
<label for="schedule_mapping_file">Replace Saved QBO Mapping File</label>
<input type="file" id="schedule_mapping_file" name="mapping_file" accept=".csv,text/csv">
<div class="helper-text">Upload a new mapping file here if you want scheduled exports to use a different saved QBO map.</div>
</div>
<div class="saved-map" style="margin-top:16px;">
Scheduled sends:
<strong>
<?php echo !empty($scheduleSettings['times']) ? htmlspecialchars(scheduledExportReadableTimes((array) $scheduleSettings['times']), ENT_QUOTES, 'UTF-8') : 'Not scheduled'; ?>
</strong>
<br>Last sent:
<strong>
<?php echo !empty($scheduleSettings['last_sent_at']) ? htmlspecialchars((string) $scheduleSettings['last_sent_at'], ENT_QUOTES, 'UTF-8') : 'Not sent yet'; ?>
</strong>
</div>
<button type="submit" class="btn">Save Export Schedule</button>
<?php endif; ?>
</form>
<a href="index.php" class="back-link">← Back to Dashboard</a>
</div>
</body>
</html>