api/quote_pdf.php.bak_20260428_183907_svctotal

Code: DEV-138C71E9 Size: 15.3 KB Lines: 305 Path: /home/prodconfig.wenesthosting.com/dev.solargroup.wenest.se/api/quote_pdf.php.bak_20260428_183907_svctotal

Task / Comment

Open report form
<?php
/**
 * /api/quote_pdf.php?id=<quoteId>
 * Levererar en utskriftsvänlig HTML-sida för offerten.
 * Browser "Save as PDF" → färdig PDF.
 */
require_once __DIR__ . '/config.php';

$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$inModal = !empty($_GET['modal']);
if (!$id) { http_response_code(400); echo 'id saknas'; exit; }

$db = getDB();
$stmt = $db->prepare('SELECT * FROM quotes WHERE id = ?');
$stmt->execute([$id]);
$q = $stmt->fetch();
if (!$q) { http_response_code(404); echo 'Offert ej hittad'; exit; }

$config = json_decode($q['config_data'] ?? '[]', true) ?: [];
$items  = $config['items'] ?? [];
$images = json_decode($q['images'] ?? '[]', true) ?: [];

function esc($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
function fmt($n){ return number_format((float)$n, 0, ',', ' '); }

// Summera totals + potentiella skatteavdrag per tax_type
$grandTotal = 0;
$breakdown = ['ROT' => 0.0, 'GT20' => 0.0, 'GT50' => 0.0];
$aggTaxType = 'NONE';
foreach ($items as $cat => $item) {
    if ($cat === '_addonsState' || (strlen($cat) > 0 && $cat[0] === '_' && preg_match('/State$/', $cat))) continue;
    $entries = isset($item['entries']) && is_array($item['entries']) ? $item['entries'] : [$item];
    foreach ($entries as $e) {
        $grandTotal += (float) ($e['subtotal'] ?? $e['total'] ?? 0);
        $ded = (float) ($e['deduction_amount'] ?? 0);
        $tt  = (string) ($e['tax_type'] ?? 'NONE');
        if ($ded > 0 && isset($breakdown[$tt])) $breakdown[$tt] += $ded;
        if ($aggTaxType === 'NONE' && $tt !== 'NONE') $aggTaxType = $tt;
    }
}
$potentialDed = $breakdown['ROT'] + $breakdown['GT20'] + $breakdown['GT50'];

// Hämta skatteinställningar från app_settings
$taxRow = $db->query("SELECT setting_key, setting_value FROM app_settings WHERE setting_key IN ('tax_rot_max_per_person','tax_rot_percent','tax_gt50_percent','tax_gt20_percent','tax_moms_percent')")->fetchAll();
$tax = ['rotMax'=>50000,'rot'=>30,'gt50'=>50,'gt20'=>20,'moms'=>25];
foreach ($taxRow as $r) {
    $v = (float) $r['setting_value'];
    if ($r['setting_key'] === 'tax_rot_max_per_person') $tax['rotMax'] = $v;
    elseif ($r['setting_key'] === 'tax_rot_percent') $tax['rot']  = $v;
    elseif ($r['setting_key'] === 'tax_gt50_percent') $tax['gt50'] = $v;
    elseif ($r['setting_key'] === 'tax_gt20_percent') $tax['gt20'] = $v;
    elseif ($r['setting_key'] === 'tax_moms_percent') $tax['moms'] = $v;
}

$owners       = max(1, (int) ($q['owner_count'] ?? 1));
$maxPerPerson = ($aggTaxType === 'ROT') ? $tax['rotMax'] : 50000;
$ownerCap     = $owners * $maxPerPerson;
$effectiveDed = min($potentialDed, $ownerCap);
$netTotal     = $grandTotal - $effectiveDed;

$deductionLabel = '';
if ($aggTaxType === 'GT50') $deductionLabel = 'Grönt teknik-avdrag ('.$tax['gt50'].'%)';
elseif ($aggTaxType === 'GT20') $deductionLabel = 'Grönt teknik-avdrag ('.$tax['gt20'].'%)';
elseif ($aggTaxType === 'ROT') $deductionLabel = 'ROT-avdrag ('.$tax['rot'].'%)';
elseif ($effectiveDed > 0) $deductionLabel = 'Skattereduktion';

$catLabels = [
    'solceller'=>'Solpaneler','batteri'=>'Batteri','laddbox'=>'Laddbox',
    'taktvatt'=>'Taktvätt','varmepump'=>'Värmepump','tak'=>'Tak',
    'fonster'=>'Fönster','isolering'=>'Isolering','_addons'=>'Övriga installationskostnader'
];

// Hämta företagsinfo från app_settings
$companyKeys = ['company_name','company_orgnr','company_address','company_phone','company_email','company_website','company_footer','company_logo'];
$placeholders = implode(',', array_fill(0, count($companyKeys), '?'));
$cstmt = $db->prepare("SELECT setting_key, setting_value FROM app_settings WHERE setting_key IN ($placeholders)");
$cstmt->execute($companyKeys);
$company = [];
while ($row = $cstmt->fetch()) { $company[$row['setting_key']] = $row['setting_value']; }

$cName    = $company['company_name']    ?? 'Solar Energy Group';
$cOrgnr   = $company['company_orgnr']   ?? '';
$cAddress = $company['company_address'] ?? '';
$cPhone   = $company['company_phone']   ?? '';
$cEmail   = $company['company_email']   ?? '';
$cWebsite = $company['company_website'] ?? '';
$cFooter  = $company['company_footer']  ?? 'Offerten är giltig 30 dagar från datum ovan.';
$cLogo    = $company['company_logo']    ?? '';
?><!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="utf-8">
<title>Offert #<?= esc($q['id']) ?><?= $q['customer_name'] ? ' – '.esc($q['customer_name']) : '' ?></title>
<style>
  @page { size: A4; margin: 20mm 15mm; }
  *{ box-sizing:border-box; }
  body{ font-family: -apple-system, Segoe UI, Roboto, Arial, sans-serif; color:#0f172a; margin:0; padding:24px; font-size:13px; line-height:1.5; }
  .pdf-head{ display:flex; justify-content:space-between; align-items:flex-start; border-bottom:3px solid #024550; padding-bottom:14px; margin-bottom:20px; }
  .pdf-head h1{ margin:0 0 4px; font-size:22px; color:#024550; }
  .pdf-head .meta{ text-align:right; font-size:12px; color:#64748b; }
  .pdf-customer{ display:grid; grid-template-columns:1fr 1fr; gap:6px 20px; padding:12px 16px; background:#f8fafc; border:1px solid #e5e7eb; border-radius:8px; margin-bottom:20px; font-size:12px; }
  .pdf-customer .lbl{ color:#64748b; font-weight:700; text-transform:uppercase; letter-spacing:.4px; font-size:10px; }
  .pdf-section{ margin-bottom:18px; }
  .pdf-section h2{ font-size:13px; margin:0 0 8px; padding:6px 12px; background:#024550; color:#fff; text-transform:uppercase; letter-spacing:.5px; border-radius:6px; display:flex; justify-content:space-between; }
  table.pdf-lines{ width:100%; border-collapse:collapse; }
  table.pdf-lines td{ padding:8px 12px; border-bottom:1px solid #e5e7eb; vertical-align:top; font-size:12px; }
  table.pdf-lines td:last-child{ text-align:right; white-space:nowrap; font-weight:700; color:#024550; }
  .pdf-sub{ color:#64748b; font-size:11px; margin-top:2px; }
  .pdf-totals{ margin-top:24px; margin-left:auto; width:320px; border:2px solid #024550; border-radius:10px; overflow:hidden; }
  .pdf-totals-row{ display:flex; justify-content:space-between; padding:10px 14px; border-bottom:1px solid #e5e7eb; font-size:13px; }
  .pdf-totals-row:last-child{ border-bottom:none; background:#024550; color:#fff; font-size:16px; font-weight:800; }
  .pdf-images{ display:grid; grid-template-columns:repeat(3,1fr); gap:10px; margin-top:20px; }
  .pdf-images img{ width:100%; height:120px; object-fit:cover; border-radius:6px; border:1px solid #e5e7eb; }
  .pdf-footer{ margin-top:40px; padding-top:14px; border-top:1px solid #e5e7eb; text-align:center; font-size:11px; color:#64748b; line-height:1.6; }
  .pdf-footer > div{ margin-bottom:4px; }
  .pdf-print-btn{ position:fixed; top:20px; right:20px; padding:10px 18px; background:#024550; color:#fff; border:none; border-radius:8px; font-size:14px; font-weight:700; cursor:pointer; box-shadow:0 4px 12px rgba(0,0,0,.15); }
  @media print { .pdf-print-btn{ display:none; } body{ padding:0; } }
</style>
</head>
<body>
<?php if (!$inModal): ?>
  <button class="pdf-print-btn" onclick="window.print()">Skriv ut / Spara som PDF</button>
<?php endif; ?>

  <div class="pdf-head">
    <div style="display:flex;align-items:center;gap:14px">
<?php if ($cLogo): ?>
      <img src="<?= esc($cLogo) ?>" alt="<?= esc($cName) ?>" style="max-height:64px;max-width:200px;object-fit:contain">
<?php endif; ?>
      <div>
        <h1>Offert</h1>
        <div style="font-size:12px;color:#64748b"><?= esc($cName) ?><?= $cOrgnr ? ' · Org.nr '.esc($cOrgnr) : '' ?></div>
      </div>
    </div>
    <div class="meta">
      <div><strong>Offert-nr:</strong> <?= esc($q['quote_number'] ?: ('#'.$q['id'])) ?></div>
      <div><strong>Datum:</strong> <?= esc(date('Y-m-d', strtotime($q['created_at']))) ?></div>
      <div><strong>Status:</strong> <?= esc(ucfirst($q['status'] ?: 'utkast')) ?></div>
    </div>
  </div>

  <div class="pdf-customer">
    <div><div class="lbl">Kund</div><div><?= esc($q['customer_name'] ?: '—') ?></div></div>
    <div><div class="lbl">Personnummer</div><div><?= esc($q['customer_personnummer'] ?: '—') ?></div></div>
    <div><div class="lbl">Adress</div><div><?= esc($q['customer_address'] ?: '—') ?></div></div>
    <div><div class="lbl">Email</div><div><?= esc($q['customer_email'] ?: '—') ?></div></div>
    <div><div class="lbl">Telefon</div><div><?= esc($q['customer_phone'] ?: '—') ?></div></div>
  </div>

<?php
// === Bulk-fetcha tillval-produkter för att veta vilka är tjänster (cat='tjanster' = ROT-relevanta arbetskostnader) ===
$tillvalIds = [];
foreach ($items as $itx) {
    $entriesX = isset($itx['entries']) && is_array($itx['entries']) ? $itx['entries'] : [$itx];
    foreach ($entriesX as $eX) {
        if (empty($eX['tillval']) || !is_array($eX['tillval'])) continue;
        foreach ($eX['tillval'] as $tvX) {
            // Vi behöver ta med ALLA (även hidden) för att kunna avgöra om de är tjänster
            if (!empty($tvX['id']) && empty($tvX['isInfo'])) $tillvalIds[$tvX['id']] = true;
        }
    }
}
$tillvalCatMap = [];
if (!empty($tillvalIds)) {
    $ids = array_keys($tillvalIds);
    $placeholders = implode(',', array_fill(0, count($ids), '?'));
    $st = $db->prepare("SELECT id, cat FROM products WHERE id IN ($placeholders)");
    $st->execute($ids);
    foreach ($st->fetchAll() as $r) $tillvalCatMap[$r['id']] = $r['cat'];
}
function _isTjanst($tv, $catMap){
    $id = $tv['id'] ?? '';
    if (isset($catMap[$id]) && $catMap[$id] === 'tjanster') return true;
    // Fallback: TJ-prefix indikerar tjänst om DB-lookup saknas
    if (strpos($id, 'TJ-') === 0) return true;
    return false;
}
?>
<?php foreach ($items as $cat => $item):
    $entriesAll = isset($item['entries']) && is_array($item['entries']) ? $item['entries'] : [$item];
    // Filtrera bort dolda addon-entries (cost-only, syns ej i offert)
    $entries = array_values(array_filter($entriesAll, function($e){ return empty($e['hidden']); }));
    if (!$entries) continue;
    $catTotal = 0;
    foreach ($entries as $e) { $catTotal += (float) ($e['subtotal'] ?? $e['total'] ?? 0); }
    $catLbl = $catLabels[$cat] ?? ucfirst($cat);

    // Gruppera fönster-rader: identiska produkt+variant räknas som EN rad med antal.
    // Tjänst-tillval (Installation/Bortforsling etc) lyfts ut till EGNA rader med pris.
    $groups = [];
    $services = []; // id => {name, count, totalPrice}
    foreach ($entries as $e) {
        $key = ($e['product_id'] ?? '') . '|' . ($e['variant_label'] ?? '');
        if (!isset($groups[$key])) {
            $groups[$key] = [
                'desc'    => $e['description'] ?? $e['product_name'] ?? $catLbl,
                'variant' => $e['variant_label'] ?? '',
                'count'   => 0,
            ];
        }
        $groups[$key]['count'] += max(1, (int) ($e['qty'] ?? 1));
        if (!empty($e['tillval']) && is_array($e['tillval'])) {
            foreach ($e['tillval'] as $tv) {
                if (!empty($tv['isInfo'])) continue;
                if (!_isTjanst($tv, $tillvalCatMap)) continue;
                // Tjänster visas ALLTID som ROT-rad — även om dolda i kalkylator-UI
                // (kunden ska se vad de betalar för i offerten)
                $sid = $tv['id'] ?? $tv['name'];
                if (!isset($services[$sid])) {
                    $services[$sid] = ['name' => $tv['name'] ?? $sid, 'count' => 0, 'total' => 0.0];
                }
                $services[$sid]['count'] += max(1, (float) ($tv['qty'] ?? 1));
                $services[$sid]['total'] += (float) ($tv['total'] ?? $tv['price'] ?? 0);
            }
        }
    }
?>
    <div class="pdf-section">
      <h2><span><?= esc($catLbl) ?></span><span><?= fmt($catTotal) ?> kr</span></h2>
      <table class="pdf-lines">
<?php foreach ($groups as $g):
        $showVariant = $g['variant'] && strpos($g['desc'], $g['variant']) === false;
?>
        <tr>
          <td style="text-align:left">
            <div><strong><?= esc($g['count']) ?> st</strong> <?= esc($g['desc']) ?><?= $showVariant ? ' <span style="color:#64748b">('.esc($g['variant']).')</span>' : '' ?></div>
          </td>
        </tr>
<?php endforeach; ?>
<?php foreach ($services as $sv):
        $cnt = $sv['count'];
        $cntTxt = ($cnt == (int)$cnt) ? (int)$cnt : number_format($cnt, 1, ',', ' ');
?>
        <tr>
          <td style="text-align:left">
            <div><strong><?= esc($cntTxt) ?> st</strong> <?= esc($sv['name']) ?> <span style="color:#64748b">(ROT-berättigad arbetskostnad)</span></div>
          </td>
          <td style="text-align:right;white-space:nowrap;font-weight:700;color:#024550"><?= fmt($sv['total']) ?> kr</td>
        </tr>
<?php endforeach; ?>
      </table>
    </div>
<?php endforeach; ?>

  <div class="pdf-totals">
    <div class="pdf-totals-row"><span>Totalt före avdrag</span><span><?= fmt($grandTotal) ?> kr</span></div>
<?php
    // Flera avdragstyper → visa en rad per typ (oskapet — cap appliceras separat mot totalen).
    $dedTypesActive = ((int)($breakdown['ROT']  > 0) + (int)($breakdown['GT20'] > 0) + (int)($breakdown['GT50'] > 0));
?>
<?php if ($dedTypesActive > 1): ?>
    <?php if ($breakdown['ROT'] > 0): ?>
    <div class="pdf-totals-row"><span>ROT-avdrag (<?= (int)$tax['rot'] ?>%)</span><span style="color:#059669">−<?= fmt($breakdown['ROT']) ?> kr</span></div>
    <?php endif; ?>
    <?php if ($breakdown['GT20'] > 0): ?>
    <div class="pdf-totals-row"><span>Grönt teknik-avdrag (<?= (int)$tax['gt20'] ?>%)</span><span style="color:#059669">−<?= fmt($breakdown['GT20']) ?> kr</span></div>
    <?php endif; ?>
    <?php if ($breakdown['GT50'] > 0): ?>
    <div class="pdf-totals-row"><span>Skattereduktion (<?= (int)$tax['gt50'] ?>%)</span><span style="color:#059669">−<?= fmt($breakdown['GT50']) ?> kr</span></div>
    <?php endif; ?>
    <div class="pdf-totals-row"><span>Totalt avdrag<?= $owners > 1 ? ' · '.$owners.' ägare' : '' ?></span><span style="color:#059669;font-weight:700">−<?= fmt($effectiveDed) ?> kr</span></div>
<?php elseif ($effectiveDed > 0): ?>
    <div class="pdf-totals-row"><span><?= esc($deductionLabel ?: 'Skattereduktion') ?><?= $owners > 1 ? ' · '.$owners.' ägare' : '' ?></span><span style="color:#059669">−<?= fmt($effectiveDed) ?> kr</span></div>
<?php endif; ?>
<?php if ($potentialDed > $effectiveDed): ?>
    <div class="pdf-totals-row" style="font-size:11px;color:#94a3b8"><span>Kvarvarande potentiellt avdrag</span><span><?= fmt($potentialDed - $effectiveDed) ?> kr</span></div>
<?php endif; ?>
    <div class="pdf-totals-row"><span>Att betala (inkl. moms <?= (int) $tax['moms'] ?>%)</span><span><?= fmt($netTotal) ?> kr</span></div>
  </div>

<?php if (count($images)): ?>
  <div class="pdf-section" style="page-break-before:always">
    <h2>Prospektbilder</h2>
    <div class="pdf-images">
<?php foreach ($images as $im):
        $url = is_array($im) ? ($im['url'] ?? '') : $im;
        if (!$url) continue;
?>
      <img src="<?= esc($url) ?>" alt="">
<?php endforeach; ?>
    </div>
  </div>
<?php endif; ?>

<?php if (!empty($q['notes'])): ?>
  <div class="pdf-section">
    <h2>Anteckningar</h2>
    <div style="padding:10px 14px;background:#f8fafc;border:1px solid #e5e7eb;border-radius:6px;white-space:pre-wrap"><?= esc($q['notes']) ?></div>
  </div>
<?php endif; ?>

  <div class="pdf-footer">
    <div>
      <strong><?= esc($cName) ?></strong><?= $cOrgnr ? ' · Org.nr '.esc($cOrgnr) : '' ?><br>
      <?php
        $addrBits = array_filter([$cAddress, $cPhone, $cEmail, $cWebsite]);
        echo esc(implode(' · ', $addrBits));
      ?>
    </div>
    <div><?= esc($cFooter) ?></div>
  </div>

</body>
</html>