| 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
require_once __DIR__ . '/store_settings.php';
require_once __DIR__ . '/qbo_name_map_helpers.php';
function scheduledExportAddonEnabled(mysqli $conn): bool
{
if (defined('SELF_HOSTED_MODE') && SELF_HOSTED_MODE) {
return true;
}
return getAppSettingValue($conn, 'scheduled_export_addon_enabled', '0') === '1';
}
function scheduledExportDefaultTimes(int $timesPerDay): array
{
$map = [
1 => ['09:00'],
2 => ['09:00', '14:00'],
3 => ['09:00', '13:00', '17:00'],
4 => ['08:00', '11:00', '14:00', '17:00'],
5 => ['08:00', '10:30', '13:00', '15:30', '18:00'],
];
return $map[$timesPerDay] ?? [];
}
function scheduledExportNormalizeTime(string $time): string
{
$time = trim($time);
if ($time === '') {
return '';
}
if (!preg_match('/^(\d{1,2}):(\d{2})$/', $time, $matches)) {
throw new RuntimeException('Please enter each scheduled export time in HH:MM format.');
}
$hour = (int) $matches[1];
$minute = (int) $matches[2];
if ($hour < 0 || $hour > 23 || $minute < 0 || $minute > 59) {
throw new RuntimeException('Please choose valid scheduled export times.');
}
return sprintf('%02d:%02d', $hour, $minute);
}
function scheduledExportTimesFromCsv(string $csv, int $timesPerDay): array
{
$times = [];
foreach (explode(',', $csv) as $part) {
$part = trim($part);
if ($part === '') {
continue;
}
$normalized = scheduledExportNormalizeTime($part);
if (!in_array($normalized, $times, true)) {
$times[] = $normalized;
}
}
if (count($times) === 0 && $timesPerDay > 0) {
$times = scheduledExportDefaultTimes($timesPerDay);
}
return array_slice($times, 0, max(0, $timesPerDay));
}
function scheduledExportTimesForSave(array $settings, int $timesPerDay): array
{
$rawTimes = $settings['times'] ?? [];
$times = [];
if (!is_array($rawTimes)) {
$rawTimes = [];
}
for ($i = 0; $i < $timesPerDay; $i++) {
$value = isset($rawTimes[$i]) ? (string) $rawTimes[$i] : '';
$normalized = scheduledExportNormalizeTime($value);
if ($normalized === '') {
throw new RuntimeException('Please choose all scheduled send times before saving.');
}
if (in_array($normalized, $times, true)) {
throw new RuntimeException('Each scheduled send time must be different.');
}
$times[] = $normalized;
}
sort($times);
return $times;
}
function scheduledExportFormatTimeLabel(string $time): string
{
$timestamp = strtotime('1970-01-01 ' . $time . ':00');
return $timestamp ? date('g:i A', $timestamp) : $time;
}
function scheduledExportReadableTimes(array $times): string
{
if (!$times) {
return 'No send times selected';
}
$labels = array_map('scheduledExportFormatTimeLabel', $times);
return implode(', ', $labels);
}
function scheduledExportSettings(mysqli $conn, string $defaultEmail = ''): array
{
$timesPerDay = (int) getAppSettingValue($conn, 'scheduled_export_times_per_day', '0');
if ($timesPerDay < 0) {
$timesPerDay = 0;
}
if ($timesPerDay > 5) {
$timesPerDay = 5;
}
$times = scheduledExportTimesFromCsv(
getAppSettingValue($conn, 'scheduled_export_times_csv', ''),
$timesPerDay
);
return [
'addon_enabled' => scheduledExportAddonEnabled($conn),
'enabled' => getAppSettingValue($conn, 'scheduled_export_enabled', '0') === '1',
'email' => getAppSettingValue($conn, 'scheduled_export_email', $defaultEmail),
'times_per_day' => $timesPerDay,
'times' => $times,
'include_archived' => getAppSettingValue($conn, 'scheduled_export_include_archived', '1') === '1',
'map_qbo_names' => getAppSettingValue($conn, 'scheduled_export_map_qbo_names', '0') === '1',
'last_sent_at' => getAppSettingValue($conn, 'scheduled_export_last_sent_at', ''),
'last_sent_marker' => getAppSettingValue($conn, 'scheduled_export_last_sent_marker', ''),
];
}
function saveScheduledExportSettings(mysqli $conn, array $settings): void
{
if (!scheduledExportAddonEnabled($conn)) {
throw new RuntimeException('Scheduled email exports are available as a paid add-on.');
}
$timesPerDay = (int) ($settings['times_per_day'] ?? 0);
$timesPerDay = max(0, min($timesPerDay, 5));
$times = $timesPerDay > 0 ? scheduledExportTimesForSave($settings, $timesPerDay) : [];
setAppSettingValue($conn, 'scheduled_export_enabled', !empty($settings['enabled']) ? '1' : '0');
setAppSettingValue($conn, 'scheduled_export_email', trim((string) ($settings['email'] ?? '')));
setAppSettingValue($conn, 'scheduled_export_times_per_day', (string) $timesPerDay);
setAppSettingValue($conn, 'scheduled_export_times_csv', implode(',', $times));
setAppSettingValue($conn, 'scheduled_export_include_archived', !empty($settings['include_archived']) ? '1' : '0');
setAppSettingValue($conn, 'scheduled_export_map_qbo_names', !empty($settings['map_qbo_names']) ? '1' : '0');
}
function scheduledExportDueMarker(array $settings, ?int $now = null): ?array
{
if (empty($settings['addon_enabled']) || empty($settings['enabled']) || empty($settings['email']) || empty($settings['times'])) {
return null;
}
$now = $now ?? time();
$lastSentMarker = trim((string) ($settings['last_sent_marker'] ?? ''));
$today = date('Y-m-d', $now);
$due = null;
foreach ((array) ($settings['times'] ?? []) as $time) {
$slotTs = strtotime($today . ' ' . $time . ':00');
if ($slotTs === false || $slotTs > $now) {
continue;
}
$marker = $today . ' ' . $time;
if ($lastSentMarker !== '' && strcmp($marker, $lastSentMarker) <= 0) {
continue;
}
$due = [
'marker' => $marker,
'time' => $time,
];
}
return $due;
}
function scheduledExportIsDue(array $settings, ?int $now = null): bool
{
return scheduledExportDueMarker($settings, $now) !== null;
}
function scheduledExportMarkSent(mysqli $conn, ?string $sentAt = null, ?string $marker = null): void
{
setAppSettingValue($conn, 'scheduled_export_last_sent_at', $sentAt ?: date('Y-m-d H:i:s'));
if ($marker !== null && $marker !== '') {
setAppSettingValue($conn, 'scheduled_export_last_sent_marker', $marker);
}
}
function exportRowsForMonth(mysqli $conn, string $month, bool $includeArchived): array
{
if (!preg_match('/^\d{4}-\d{2}$/', $month)) {
throw new RuntimeException('Please choose a valid export month.');
}
$startDate = $month . '-01';
$endDate = date('Y-m-t', strtotime($startDate));
$rows = [];
$currentStmt = $conn->prepare("
SELECT
t.date AS transaction_date,
c.first_name,
c.last_name,
t.item_name,
t.quantity,
t.price,
t.taxable,
t.notes,
'current' AS source_type
FROM transactions t
INNER JOIN customers c ON c.id = t.customer_id
WHERE t.date BETWEEN ? AND ?
ORDER BY t.date ASC, c.last_name ASC, c.first_name ASC, t.id ASC
");
$currentStmt->bind_param('ss', $startDate, $endDate);
$currentStmt->execute();
$currentResult = $currentStmt->get_result();
while ($row = $currentResult->fetch_assoc()) {
$rows[] = $row;
}
$currentStmt->close();
if ($includeArchived) {
$archivedStmt = $conn->prepare("
SELECT
a.original_date AS transaction_date,
c.first_name,
c.last_name,
a.item_name,
a.quantity,
a.price,
a.taxable,
a.notes,
'archived' AS source_type
FROM archived_transactions a
INNER JOIN customers c ON c.id = a.customer_id
WHERE a.original_date BETWEEN ? AND ?
ORDER BY a.original_date ASC, c.last_name ASC, c.first_name ASC, a.id ASC
");
$archivedStmt->bind_param('ss', $startDate, $endDate);
$archivedStmt->execute();
$archivedResult = $archivedStmt->get_result();
while ($row = $archivedResult->fetch_assoc()) {
$rows[] = $row;
}
$archivedStmt->close();
}
usort($rows, static function (array $a, array $b): int {
return [$a['transaction_date'], $a['last_name'], $a['first_name'], $a['item_name']]
<=>
[$b['transaction_date'], $b['last_name'], $b['first_name'], $b['item_name']];
});
return $rows;
}
function exportCsvStringForMonth(mysqli $conn, string $month, bool $includeArchived, bool $mapQboNames): string
{
$rows = exportRowsForMonth($conn, $month, $includeArchived);
$taxRate = getStoreTaxRate($conn);
$qboNameMap = $mapQboNames ? qboNameMapLoadSaved($conn) : [];
$output = fopen('php://temp', 'r+');
if ($output === false) {
throw new RuntimeException('Could not generate CSV.');
}
fputcsv($output, [
'Date',
'Customer',
'Item',
'Quantity',
'Price',
'Subtotal',
'Taxable',
'Tax',
'Total',
'Notes',
'Source',
]);
foreach ($rows as $row) {
$quantity = (float) ($row['quantity'] ?? 0);
$price = (float) ($row['price'] ?? 0);
$subtotal = $quantity * $price;
$isTaxable = !empty($row['taxable']);
$tax = $isTaxable ? $subtotal * $taxRate : 0.0;
$total = $subtotal + $tax;
$customer = trim(((string) ($row['first_name'] ?? '')) . ' ' . ((string) ($row['last_name'] ?? '')));
$mappedCustomer = $customer;
if ($mapQboNames) {
$normalizedCustomer = strtolower(trim($customer));
if (isset($qboNameMap[$normalizedCustomer])) {
$mappedCustomer = $qboNameMap[$normalizedCustomer];
}
}
fputcsv($output, [
(string) ($row['transaction_date'] ?? ''),
$mappedCustomer,
(string) ($row['item_name'] ?? ''),
number_format($quantity, 2, '.', ''),
number_format($price, 2, '.', ''),
number_format($subtotal, 2, '.', ''),
$isTaxable ? 'Yes' : 'No',
number_format($tax, 2, '.', ''),
number_format($total, 2, '.', ''),
(string) ($row['notes'] ?? ''),
(string) ($row['source_type'] ?? 'current'),
]);
}
rewind($output);
$csv = stream_get_contents($output);
fclose($output);
if ($csv === false) {
throw new RuntimeException('Could not generate CSV.');
}
return $csv;
}
function sendScheduledExportEmail(string $toEmail, string $storeName, string $month, string $csvContent, string $filename): bool
{
$boundary = 'housetab_export_' . md5((string) microtime(true));
$subject = 'HouseTab Pro Export - ' . $storeName . ' - ' . date('F Y', strtotime($month . '-01'));
$body = implode("\r\n", [
'Hello,',
'',
'Your scheduled HouseTab Pro export is attached.',
'',
'Store: ' . $storeName,
'Month: ' . date('F Y', strtotime($month . '-01')),
'',
'Warmly,',
'HouseTab Pro',
]);
$headers = [
'MIME-Version: 1.0',
'From: HouseTabPro <Accounts@housetabpro.com>',
'Reply-To: HouseTabPro <support@housetabpro.com>',
'X-Mailer: PHP/' . phpversion(),
'Content-Type: multipart/mixed; boundary="' . $boundary . '"',
];
$attachment = chunk_split(base64_encode($csvContent));
$message = "--{$boundary}\r\n";
$message .= "Content-Type: text/plain; charset=UTF-8\r\n";
$message .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
$message .= $body . "\r\n\r\n";
$message .= "--{$boundary}\r\n";
$message .= "Content-Type: text/csv; name=\"{$filename}\"\r\n";
$message .= "Content-Transfer-Encoding: base64\r\n";
$message .= "Content-Disposition: attachment; filename=\"{$filename}\"\r\n\r\n";
$message .= $attachment . "\r\n";
$message .= "--{$boundary}--\r\n";
return mail($toEmail, $subject, $message, implode("\r\n", $headers));
}