1385 lines
58 KiB
PHP
1385 lines
58 KiB
PHP
<?php
|
||
ob_start();
|
||
ini_set('display_errors', 1);
|
||
error_reporting(E_ALL);
|
||
|
||
include('include/headscript.php');
|
||
require_once __DIR__ . '/class/MagoSalesStats.php';
|
||
require_once __DIR__ . '/class/MagoQuote.php';
|
||
|
||
$db = DBHandlerSelect::getInstance();
|
||
$pdo = $db->getConnection();
|
||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||
|
||
if (!userCan('sales.quote.view')) {
|
||
if (isset($_GET['action']) || isset($_POST['action'])) {
|
||
MagoSalesStats::jsonOut(['success' => false, 'message' => 'Non hai i permessi per questa pagina']);
|
||
}
|
||
while (ob_get_level()) ob_end_clean();
|
||
http_response_code(403);
|
||
echo '<div style="font-family:sans-serif;padding:60px;text-align:center;"><h3>Accesso non consentito</h3><p>Non hai i permessi per visualizzare questa pagina.</p><a href="production_dashboard.php">Torna alla Dashboard</a></div>';
|
||
exit;
|
||
}
|
||
$canDeliveries = userCan('sales.deliveries.view');
|
||
$canMatrici = userCan('sales.matrici.view');
|
||
|
||
/* ============================================================
|
||
* AJAX
|
||
* ============================================================ */
|
||
$action = $_GET['action'] ?? $_POST['action'] ?? '';
|
||
try {
|
||
if ($action === 'matrici') {
|
||
MagoSalesStats::jsonOut(['results' => MagoQuote::searchMatrici($pdo, (string)($_GET['q'] ?? ''))]);
|
||
}
|
||
if ($action === 'customers') {
|
||
$f = MagoSalesStats::filterOptions($pdo);
|
||
MagoSalesStats::jsonOut(['success' => true, 'customers' => $f['customers'] ?? []]);
|
||
}
|
||
if ($action === 'data') {
|
||
$c = trim((string)($_GET['c'] ?? ''));
|
||
MagoSalesStats::jsonOut(MagoQuote::data($pdo, (int)($_GET['m'] ?? 0), (string)($_GET['u'] ?? 'm'), $c !== '' ? $c : null));
|
||
}
|
||
if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
MagoSalesStats::jsonOut(MagoQuote::save($pdo, $_POST, (int)($iduserlogin ?? 0)));
|
||
}
|
||
if ($action === 'delete' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
MagoSalesStats::jsonOut(['success' => MagoQuote::delete($pdo, (int)($_POST['id'] ?? 0))]);
|
||
}
|
||
} catch (Throwable $e) {
|
||
MagoSalesStats::jsonOut(['success' => false, 'message' => $e->getMessage()]);
|
||
}
|
||
?>
|
||
<!doctype html>
|
||
<html lang="it">
|
||
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||
<?php include('cssinclude.php'); ?>
|
||
<title>Preventivo matrice - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||
|
||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
|
||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.full.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.49.1"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||
|
||
<style>
|
||
:root {
|
||
--c-ink: #1f2d3d;
|
||
--c-muted: #64748b;
|
||
--c-line: #e2e8f0;
|
||
--pv: #0f766e;
|
||
}
|
||
|
||
body {
|
||
font-size: 1.02rem;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.card {
|
||
border-radius: 16px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, .08);
|
||
}
|
||
|
||
.back-dashboard {
|
||
background: #b9ebc7 !important;
|
||
color: var(--c-ink) !important;
|
||
border-radius: 10px;
|
||
font-weight: 600;
|
||
padding: 10px 18px;
|
||
}
|
||
|
||
.stats-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.stats-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
}
|
||
|
||
.stats-title-icon {
|
||
width: 52px;
|
||
height: 52px;
|
||
border-radius: 14px;
|
||
background: linear-gradient(135deg, #0f766e, #22c55e);
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.5rem;
|
||
box-shadow: 0 6px 16px rgba(15, 118, 110, .28);
|
||
}
|
||
|
||
.stats-tabs {
|
||
display: flex;
|
||
gap: 6px;
|
||
border-bottom: 2px solid var(--c-line);
|
||
margin-bottom: 18px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.stats-tabs a {
|
||
padding: 10px 18px;
|
||
border-radius: 12px 12px 0 0;
|
||
font-weight: 700;
|
||
color: var(--c-muted);
|
||
text-decoration: none;
|
||
background: #f1f5f9;
|
||
margin-bottom: -2px;
|
||
border: 2px solid transparent;
|
||
}
|
||
|
||
.stats-tabs a.active {
|
||
background: #fff;
|
||
color: #198754;
|
||
border-color: var(--c-line);
|
||
border-bottom-color: #fff;
|
||
}
|
||
|
||
.pv-box {
|
||
background: #fff;
|
||
border: 1px solid var(--c-line);
|
||
border-radius: 16px;
|
||
padding: 16px;
|
||
height: 100%;
|
||
}
|
||
|
||
.pv-title {
|
||
font-weight: 800;
|
||
color: var(--c-ink);
|
||
font-size: 1.02rem;
|
||
}
|
||
|
||
.pv-hint {
|
||
font-size: .82rem;
|
||
color: var(--c-muted);
|
||
}
|
||
|
||
.flabel {
|
||
font-weight: 700;
|
||
font-size: .8rem;
|
||
color: var(--c-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: .4px;
|
||
margin-bottom: 4px;
|
||
display: block;
|
||
}
|
||
|
||
.select2-container--bootstrap-5 .select2-selection {
|
||
border-radius: 10px;
|
||
min-height: 46px;
|
||
}
|
||
|
||
.seg {
|
||
display: inline-flex;
|
||
background: #f1f5f9;
|
||
border-radius: 12px;
|
||
padding: 4px;
|
||
gap: 4px;
|
||
}
|
||
|
||
.seg button {
|
||
border: 0;
|
||
background: transparent;
|
||
padding: 9px 18px;
|
||
border-radius: 9px;
|
||
font-weight: 700;
|
||
color: var(--c-muted);
|
||
}
|
||
|
||
.seg button.active {
|
||
background: #fff;
|
||
color: var(--c-ink);
|
||
box-shadow: 0 2px 6px rgba(15, 23, 42, .1);
|
||
}
|
||
|
||
.mx-opt {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.mx-opt img,
|
||
.mx-opt .ph {
|
||
width: 34px;
|
||
height: 34px;
|
||
border-radius: 8px;
|
||
object-fit: cover;
|
||
background: #f1f5f9;
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.pv-head {
|
||
display: flex;
|
||
gap: 14px;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.pv-photo {
|
||
width: 74px;
|
||
height: 74px;
|
||
border-radius: 14px;
|
||
object-fit: cover;
|
||
border: 1px solid var(--c-line);
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.pv-name {
|
||
font-size: 1.3rem;
|
||
font-weight: 800;
|
||
color: var(--c-ink);
|
||
}
|
||
|
||
.tb {
|
||
display: inline-block;
|
||
font-size: .7rem;
|
||
font-weight: 800;
|
||
border-radius: 999px;
|
||
padding: 2px 8px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.tb-warn {
|
||
background: #fee2e2;
|
||
color: #991b1b;
|
||
}
|
||
|
||
.tb-old {
|
||
background: #fef3c7;
|
||
color: #92400e;
|
||
}
|
||
|
||
.tb-check {
|
||
background: #ffedd5;
|
||
color: #9a3412;
|
||
}
|
||
|
||
.tb-ok {
|
||
background: #dcfce7;
|
||
color: #166534;
|
||
}
|
||
|
||
.pv-mix th {
|
||
font-size: .78rem;
|
||
color: var(--c-muted);
|
||
text-transform: uppercase;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.pv-mix input {
|
||
width: 92px;
|
||
text-align: right;
|
||
border-radius: 8px;
|
||
padding: 4px 8px;
|
||
border: 1px solid #cbd5e1;
|
||
min-height: 38px;
|
||
}
|
||
|
||
.pv-mix input.missing {
|
||
border-color: #dc2626;
|
||
background: #fef2f2;
|
||
}
|
||
|
||
.pv-mix input.edited {
|
||
background: #fffbeb;
|
||
border-color: #f59e0b;
|
||
}
|
||
|
||
.num {
|
||
font-variant-numeric: tabular-nums;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.pv-sum {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
border-radius: 12px;
|
||
background: #f8fafc;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.pv-sum.total {
|
||
background: #fef2f2;
|
||
border: 1px solid #fecaca;
|
||
}
|
||
|
||
.pv-sum b {
|
||
font-size: 1.15rem;
|
||
}
|
||
|
||
.pv-in {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.pv-in input {
|
||
max-width: 130px;
|
||
text-align: right;
|
||
font-weight: 700;
|
||
border-radius: 10px;
|
||
min-height: 44px;
|
||
}
|
||
|
||
.pv-big {
|
||
font-size: 2.3rem;
|
||
font-weight: 900;
|
||
color: var(--pv);
|
||
line-height: 1.1;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.chips {
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.chip-mk {
|
||
border: 1.5px solid var(--c-line);
|
||
background: #fff;
|
||
border-radius: 999px;
|
||
padding: 7px 13px;
|
||
font-weight: 700;
|
||
font-size: .88rem;
|
||
min-height: 40px;
|
||
}
|
||
|
||
.chip-mk.active {
|
||
background: var(--pv);
|
||
border-color: var(--pv);
|
||
color: #fff;
|
||
}
|
||
|
||
.ref-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.ref {
|
||
border: 1px solid var(--c-line);
|
||
border-radius: 12px;
|
||
padding: 10px 12px;
|
||
background: #fff;
|
||
}
|
||
|
||
.ref .l {
|
||
font-size: .74rem;
|
||
font-weight: 800;
|
||
text-transform: uppercase;
|
||
color: var(--c-muted);
|
||
letter-spacing: .3px;
|
||
}
|
||
|
||
.ref .v {
|
||
font-size: 1.15rem;
|
||
font-weight: 800;
|
||
color: var(--c-ink);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.ref .s {
|
||
font-size: .8rem;
|
||
color: var(--c-muted);
|
||
}
|
||
|
||
.dp {
|
||
font-weight: 800;
|
||
}
|
||
|
||
.dp.up {
|
||
color: #15803d;
|
||
}
|
||
|
||
.dp.down {
|
||
color: #b91c1c;
|
||
}
|
||
|
||
.mg {
|
||
display: inline-block;
|
||
font-weight: 800;
|
||
border-radius: 999px;
|
||
padding: 3px 11px;
|
||
}
|
||
|
||
.mg-hi {
|
||
background: #dcfce7;
|
||
color: #166534;
|
||
}
|
||
|
||
.mg-mid {
|
||
background: #fef9c3;
|
||
color: #854d0e;
|
||
}
|
||
|
||
.mg-lo {
|
||
background: #fee2e2;
|
||
color: #991b1b;
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 50px 20px;
|
||
color: var(--c-muted);
|
||
}
|
||
|
||
.q-table td,
|
||
.q-table th {
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.q-table thead th {
|
||
background: #ccfbf1;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.btn-pv {
|
||
background: var(--pv);
|
||
color: #fff;
|
||
border-radius: 10px;
|
||
font-weight: 700;
|
||
padding: 10px 18px;
|
||
}
|
||
|
||
.btn-pv:hover {
|
||
background: #115e59;
|
||
color: #fff;
|
||
}
|
||
|
||
@media print {
|
||
|
||
.no-print,
|
||
.sidebar-wrapper,
|
||
.topbar,
|
||
.page-footer,
|
||
.stats-tabs,
|
||
#pvPick,
|
||
#pvHistory {
|
||
display: none !important;
|
||
}
|
||
|
||
.page-wrapper {
|
||
margin: 0 !important;
|
||
}
|
||
|
||
body {
|
||
background: #fff;
|
||
}
|
||
|
||
.card {
|
||
box-shadow: none;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="wrapper toggled" id="appWrapper">
|
||
<?php include('include/navbar.php'); ?>
|
||
<?php include('include/topbar.php'); ?>
|
||
|
||
<div class="page-wrapper">
|
||
<div class="page-content">
|
||
<div class="card p-3">
|
||
<div class="card-header bg-white border-0 pb-3">
|
||
<div class="stats-header">
|
||
<div class="stats-title">
|
||
<div class="stats-title-icon"><i class="fa-solid fa-calculator"></i></div>
|
||
<div>
|
||
<h5 class="mb-1" style="font-weight:800;">Preventivo matrice</h5>
|
||
<div class="pv-hint">Costo della mescola, prezzi praticati e prezzo suggerito</div>
|
||
</div>
|
||
</div>
|
||
<button type="button" class="btn back-dashboard no-print" onclick="location.href='production_dashboard.php'">↩️ Torna alla Dashboard</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-body pt-0">
|
||
<div class="stats-tabs">
|
||
<a href="sales_summary.php"><i class="fa-solid fa-gauge-high me-1"></i> Sommario</a>
|
||
<a href="sales_orders_stats.php"><i class="fa-solid fa-list-check me-1"></i> Andamento ordini</a>
|
||
<a href="sales_orders_years.php"><i class="fa-solid fa-chart-column me-1"></i> Confronto anni</a>
|
||
<a href="sales_customer.php"><i class="fa-solid fa-address-card me-1"></i> Scheda cliente</a>
|
||
<?php if ($canMatrici) : ?>
|
||
<a href="sales_matrici.php"><i class="fa-solid fa-shapes me-1"></i> Matrici</a>
|
||
<?php endif; ?>
|
||
<a href="sales_preventivo.php" class="active"><i class="fa-solid fa-calculator me-1"></i> Preventivo</a>
|
||
<?php if ($canDeliveries) : ?>
|
||
<a href="sales_orders_deliveries.php"><i class="fa-solid fa-truck-fast me-1"></i> Consegne</a>
|
||
<?php endif; ?>
|
||
<button type="button" class="sh-page ms-auto align-self-center mb-1" data-help-open="page.quote"><i class="fa-regular fa-circle-question me-1"></i> Come si usa</button>
|
||
</div>
|
||
|
||
<!-- SCELTA -->
|
||
<div class="pv-box mb-3" id="pvPick" style="height:auto;">
|
||
<div class="row g-3 align-items-end">
|
||
<div class="col-lg-5">
|
||
<label class="flabel" for="pvMatrice">Matrice</label>
|
||
<select id="pvMatrice" style="width:100%;"></select>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<label class="flabel" for="pvCustomer">Cliente <span class="text-muted text-lowercase fw-normal">(facoltativo)</span></label>
|
||
<select id="pvCustomer" style="width:100%;"></select>
|
||
</div>
|
||
<div class="col-lg-3">
|
||
<label class="flabel">Unità di vendita</label>
|
||
<div class="seg" id="pvUom">
|
||
<button type="button" data-u="m" class="active">€ al metro</button>
|
||
<button type="button" data-u="kg">€ al kg</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="pvEmpty" class="empty-state">
|
||
<i class="fa-solid fa-calculator fa-2x mb-2"></i>
|
||
<div>Scegli una matrice per calcolare il preventivo.</div>
|
||
</div>
|
||
<div id="pvLoading" class="empty-state" style="display:none;">
|
||
<div class="spinner-border text-secondary"></div>
|
||
</div>
|
||
|
||
<div id="pvContent" style="display:none;">
|
||
<div class="row g-3 mb-3">
|
||
<!-- COSTO -->
|
||
<div class="col-xl-5">
|
||
<div class="pv-box">
|
||
<div class="pv-head" id="pvHead"></div>
|
||
<div class="pv-title" data-help="pv.cost">1 · Costo</div>
|
||
<div class="pv-hint mb-2" id="pvCostHint"></div>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm align-middle pv-mix mb-1">
|
||
<thead>
|
||
<tr>
|
||
<th>Mescola</th>
|
||
<th class="text-end">g/m</th>
|
||
<th class="text-end">€/kg</th>
|
||
<th class="text-end" id="pvPartTh">€/m</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="pvMixBody"></tbody>
|
||
</table>
|
||
</div>
|
||
<div id="pvWarn"></div>
|
||
<div class="pv-sum"><span>Costo mescola</span><b class="num" id="pvCostMix">—</b></div>
|
||
<div class="row g-2 mt-1">
|
||
<div class="col-6">
|
||
<label class="flabel" for="pvScrap" data-help="pv.scrap">Scarto %</label>
|
||
<div class="pv-in"><input type="number" step="0.5" min="0" class="form-control" id="pvScrap" value="0"><span>%</span></div>
|
||
</div>
|
||
<div class="col-6">
|
||
<label class="flabel" for="pvOther" data-help="pv.other">Altri costi</label>
|
||
<div class="pv-in"><input type="number" step="0.001" min="0" class="form-control" id="pvOther" value="0"><span class="pv-ul">€/m</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="pv-sum total"><span>Costo totale</span><b class="num" id="pvCostTot">—</b></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PREZZO -->
|
||
<div class="col-xl-7">
|
||
<div class="pv-box">
|
||
<div class="pv-title" data-help="pv.price">2 · Prezzo</div>
|
||
<div class="row g-3 mt-0 align-items-end">
|
||
<div class="col-md-5">
|
||
<label class="flabel" for="pvMarkup">Ricarico sul costo</label>
|
||
<div class="pv-in"><input type="number" step="1" class="form-control form-control-lg" id="pvMarkup" value="100"><span class="fw-bold">%</span></div>
|
||
</div>
|
||
<div class="col-md-7">
|
||
<div class="chips" id="pvChips"></div>
|
||
</div>
|
||
</div>
|
||
<div class="row g-3 mt-1 align-items-center">
|
||
<div class="col-md-6">
|
||
<div class="flabel">Prezzo suggerito</div>
|
||
<div class="pv-big" id="pvSuggested">—</div>
|
||
<div class="pv-hint" id="pvSuggestedHint"></div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="flabel" for="pvOffered" data-help="pv.offered">Prezzo proposto</label>
|
||
<div class="pv-in"><input type="number" step="0.001" min="0" class="form-control form-control-lg" id="pvOffered"><span class="pv-ul">€/m</span></div>
|
||
<div class="mt-1" id="pvOfferedInfo"></div>
|
||
</div>
|
||
</div>
|
||
<hr>
|
||
<div class="flabel mb-2" data-help="pv.refs">Confronto con i prezzi praticati</div>
|
||
<div class="ref-grid" id="pvRefs"></div>
|
||
<div class="mt-3">
|
||
<label class="flabel" for="pvComment">Commento</label>
|
||
<textarea class="form-control" id="pvComment" rows="2" placeholder="es. quantità annua 20.000 m, consegna mensile, mescola alternativa…"></textarea>
|
||
</div>
|
||
<div class="d-flex gap-2 mt-3 justify-content-end no-print flex-wrap">
|
||
<button type="button" class="btn btn-outline-secondary" id="pvPrint"><i class="fa-solid fa-print me-1"></i> Stampa</button>
|
||
<button type="button" class="btn btn-pv" id="pvSave"><i class="fa-solid fa-floppy-disk me-1"></i> Salva preventivo</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-xl-8">
|
||
<div class="pv-box">
|
||
<div class="pv-title" data-help="pv.chart">Costo e prezzi negli ultimi 24 mesi</div>
|
||
<div class="pv-hint">Linea rossa: costo mescola del mese · grigia: prezzo medio di tutti i clienti · viola: prezzo al cliente scelto · verde: prezzo proposto</div>
|
||
<div id="pvChart" style="min-height:330px;"></div>
|
||
</div>
|
||
</div>
|
||
<div class="col-xl-4">
|
||
<div class="pv-box">
|
||
<div class="pv-title">Prezzi agli altri clienti (12 mesi)</div>
|
||
<div class="pv-hint mb-2">Prezzo medio per cliente, i più grandi per quantità</div>
|
||
<div id="pvClients"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pv-box" id="pvHistory" style="height:auto;">
|
||
<div class="pv-title">Preventivi salvati per questa matrice</div>
|
||
<div class="pv-hint mb-2">Clic su <b>Riapri</b> per ricaricare ricarico, costi e commento.</div>
|
||
<div class="table-responsive">
|
||
<table class="table table-striped q-table mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>Data</th>
|
||
<th>Cliente</th>
|
||
<th class="text-end">Costo</th>
|
||
<th class="text-end">Ricarico</th>
|
||
<th class="text-end">Prezzo</th>
|
||
<th>Commento</th>
|
||
<th>Utente</th>
|
||
<th class="no-print"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="pvQuotes"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php include('include/footer.php'); ?>
|
||
</div>
|
||
|
||
<?php include('jsinclude.php'); ?>
|
||
<script>
|
||
(function() {
|
||
const MONTHS = ['gen', 'feb', 'mar', 'apr', 'mag', 'giu', 'lug', 'ago', 'set', 'ott', 'nov', 'dic'];
|
||
const MARKUPS = [50, 80, 100, 150, 200];
|
||
let data = null;
|
||
let chart = null;
|
||
let state = {
|
||
m: null,
|
||
c: '',
|
||
u: 'm'
|
||
};
|
||
let offeredTouched = false;
|
||
let markupTouched = false;
|
||
|
||
/* ---------- utilità ---------- */
|
||
function esc(s) {
|
||
return String(s == null ? '' : s).replace(/[&<>"']/g, function(c) {
|
||
return {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
} [c];
|
||
});
|
||
}
|
||
|
||
function num(v, d) {
|
||
return (+v || 0).toLocaleString('it-IT', {
|
||
minimumFractionDigits: d || 0,
|
||
maximumFractionDigits: d || 0
|
||
});
|
||
}
|
||
|
||
function eur(v) {
|
||
if (v === null || v === undefined || !isFinite(v)) return '—';
|
||
return num(v, Math.abs(v) >= 10 ? 2 : 3) + ' €';
|
||
}
|
||
|
||
function val(id) {
|
||
const v = parseFloat(String($(id).val()).replace(',', '.'));
|
||
return isFinite(v) ? v : null;
|
||
}
|
||
|
||
function fdate(s) {
|
||
if (!s) return '';
|
||
const p = String(s).substring(0, 10).split('-');
|
||
return p[2] + '/' + p[1] + '/' + p[0];
|
||
}
|
||
|
||
function ul() {
|
||
return state.u === 'kg' ? 'kg' : 'm';
|
||
}
|
||
|
||
function marginPill(p) {
|
||
if (p === null || !isFinite(p)) return '';
|
||
const c = p >= 60 ? 'mg-hi' : p >= 40 ? 'mg-mid' : 'mg-lo';
|
||
return '<span class="mg ' + c + '">margine ' + num(p, 0) + '%</span>';
|
||
}
|
||
|
||
function delta(a, b) {
|
||
if (!a || !b) return '';
|
||
const d = (a - b) / b * 100;
|
||
const cls = d >= 0 ? 'up' : 'down';
|
||
return '<span class="dp ' + cls + '">' + (d >= 0 ? '+' : '') + num(d, 1) + '%</span>';
|
||
}
|
||
|
||
/* ---------- URL ---------- */
|
||
function readUrl() {
|
||
const q = new URLSearchParams(location.search);
|
||
if (q.get('m')) state.m = parseInt(q.get('m'), 10) || null;
|
||
if (q.get('c')) state.c = q.get('c');
|
||
if (q.get('u') === 'kg') state.u = 'kg';
|
||
}
|
||
|
||
function writeUrl() {
|
||
const u = new URL(location.href);
|
||
['m', 'c', 'u'].forEach(function(k) {
|
||
if (state[k]) u.searchParams.set(k, state[k]);
|
||
else u.searchParams.delete(k);
|
||
});
|
||
history.replaceState(null, '', u.toString());
|
||
}
|
||
|
||
/* ---------- caricamento ---------- */
|
||
function load() {
|
||
writeUrl();
|
||
$('#pvUom button').removeClass('active').filter('[data-u="' + state.u + '"]').addClass('active');
|
||
$('.pv-ul').text('€/' + ul());
|
||
$('#pvPartTh').text('€/' + ul());
|
||
if (!state.m) {
|
||
$('#pvContent, #pvLoading').hide();
|
||
$('#pvEmpty').show();
|
||
return;
|
||
}
|
||
$('#pvEmpty').hide();
|
||
$('#pvLoading').show();
|
||
const qs = new URLSearchParams({
|
||
action: 'data',
|
||
m: state.m,
|
||
c: state.c || '',
|
||
u: state.u
|
||
});
|
||
fetch('?' + qs).then(function(r) {
|
||
return r.json();
|
||
}).then(function(d) {
|
||
$('#pvLoading').hide();
|
||
if (!d.success) {
|
||
$('#pvEmpty').show().html('<div class="text-danger">' + esc(d.message || 'Errore') + '</div>');
|
||
return;
|
||
}
|
||
data = d;
|
||
offeredTouched = false;
|
||
$('#pvContent').show();
|
||
renderHead();
|
||
renderMix();
|
||
renderChips();
|
||
renderClients();
|
||
renderQuotes();
|
||
calc();
|
||
}).catch(function(e) {
|
||
$('#pvLoading').hide();
|
||
$('#pvEmpty').show().html('<div class="text-danger">Errore nel caricamento: ' + esc(e && e.message ? e.message : e) + '</div>');
|
||
});
|
||
}
|
||
|
||
function renderHead() {
|
||
const m = data.matrice;
|
||
// nome della matrice nella tendina (arrivando dall'URL o dalla pagina Matrici)
|
||
const $sel = $('#pvMatrice');
|
||
if (String($sel.val() || '') !== String(m.id) || !$sel.find('option:selected').text() || $sel.find('option:selected').text() !== m.name) {
|
||
$sel.find('option[value="' + m.id + '"]').remove();
|
||
$sel.append(new Option(m.name, m.id, true, true)).trigger('change.select2');
|
||
}
|
||
const photo = m.photo ? '<img class="pv-photo" src="' + esc(m.photo) + '" onerror="this.style.display=\'none\'">' : '';
|
||
let ws = '<span class="tb tb-warn">nessuna scheda di lavoro</span>';
|
||
if (m.ws) ws = 'Scheda di lavoro n° ' + esc(m.ws.number) + (m.ws.rev ? ' rev. ' + esc(m.ws.rev) : '') + (m.ws.date ? ' del ' + fdate(m.ws.date) : '');
|
||
const cust = data.customer ? '<div class="pv-hint">per <b>' + esc(data.customer.name) + '</b></div>' : '';
|
||
$('#pvHead').html(photo + '<div><div class="pv-name">' + esc(m.name) + ' <span class="tb tb-ok">' + esc(data.uom) + '</span></div><div class="pv-hint">' + ws + '</div>' + cust + '</div>');
|
||
const rule = data.kind === 'm' ? 'g/m × €/kg ÷ 1000' : 'media dei €/kg pesata sui g/m';
|
||
$('#pvCostHint').html('Costo = ' + rule + '. €/kg = media pagata in Mago negli ultimi ' + data.price_months + ' mesi. <b>Puoi correggere g/m ed €/kg</b> per questo preventivo.');
|
||
}
|
||
|
||
function renderMix() {
|
||
if (!data.mix.length) {
|
||
$('#pvMixBody').html('<tr><td colspan="4" class="text-muted">Nessuna mescola collegata alla matrice: compila la scheda di lavoro, oppure usa "Altri costi".</td></tr>');
|
||
$('#pvWarn').html('');
|
||
return;
|
||
}
|
||
let html = '';
|
||
data.mix.forEach(function(x, i) {
|
||
let info = '';
|
||
if (x.eur_kg === null) info += ' <span class="tb tb-warn">senza prezzo</span>';
|
||
else {
|
||
if (x.old) info += ' <span class="tb tb-old" title="Ultimo acquisto ' + fdate(x.last_date) + '">prezzo vecchio</span>';
|
||
if (x.check) info += ' <span class="tb tb-check" title="Prezzo da verificare (conto lavoro?)">' + esc(x.check) + ' da verificare</span>';
|
||
}
|
||
const sup = x.supplier ? '<div class="pv-hint">' + esc(x.supplier) + (x.last_date ? ' · ultimo ' + fdate(x.last_date) : '') + '</div>' : '';
|
||
html += '<tr data-i="' + i + '">' +
|
||
'<td><b>' + esc(x.name) + '</b>' + info + sup + '</td>' +
|
||
'<td class="text-end"><input type="number" step="0.1" min="0" class="pv-gm" value="' + (x.gm === null ? '' : x.gm) + '" placeholder="g/m"></td>' +
|
||
'<td class="text-end"><input type="number" step="0.001" min="0" class="pv-eur" value="' + (x.eur_kg === null ? '' : x.eur_kg) + '" placeholder="€/kg"></td>' +
|
||
'<td class="text-end num pv-part">—</td>' +
|
||
'</tr>';
|
||
});
|
||
$('#pvMixBody').html(html);
|
||
}
|
||
|
||
function readMix() {
|
||
const rows = [];
|
||
$('#pvMixBody tr[data-i]').each(function() {
|
||
const i = +$(this).attr('data-i');
|
||
const x = data.mix[i];
|
||
const gm = parseFloat($(this).find('.pv-gm').val());
|
||
const e = parseFloat($(this).find('.pv-eur').val());
|
||
const g = isFinite(gm) && gm > 0 ? gm : null;
|
||
const p = isFinite(e) && e > 0 ? e : null;
|
||
rows.push({
|
||
id: x.id,
|
||
name: x.name,
|
||
gm: g,
|
||
eur_kg: p,
|
||
edited: g !== x.gm || p !== x.eur_kg,
|
||
$tr: $(this)
|
||
});
|
||
});
|
||
return rows;
|
||
}
|
||
|
||
/* ---------- calcolo ---------- */
|
||
function calc() {
|
||
if (!data) return;
|
||
const rows = readMix();
|
||
const gmTot = rows.reduce(function(a, r) {
|
||
return a + (r.gm || 0);
|
||
}, 0);
|
||
let cost = 0,
|
||
priced = 0,
|
||
missing = 0;
|
||
rows.forEach(function(r) {
|
||
let part = null;
|
||
if (data.kind === 'm') {
|
||
if (r.gm !== null && r.eur_kg !== null) part = r.gm / 1000 * r.eur_kg;
|
||
} else if (r.eur_kg !== null) {
|
||
part = (gmTot > 0 ? (r.gm || 0) / gmTot : 1 / rows.length) * r.eur_kg;
|
||
}
|
||
if (part === null) missing++;
|
||
else {
|
||
cost += part;
|
||
priced++;
|
||
}
|
||
r.$tr.find('.pv-part').html(part === null ? '<span class="text-danger">—</span>' : eur(part));
|
||
r.$tr.find('.pv-gm').toggleClass('edited', r.gm !== data.mix[+r.$tr.attr('data-i')].gm)
|
||
.toggleClass('missing', r.gm === null && (data.kind === 'm'));
|
||
r.$tr.find('.pv-eur').toggleClass('edited', r.eur_kg !== data.mix[+r.$tr.attr('data-i')].eur_kg)
|
||
.toggleClass('missing', r.eur_kg === null);
|
||
});
|
||
const costMix = priced ? cost : null;
|
||
const scrap = val('#pvScrap') || 0;
|
||
const other = val('#pvOther') || 0;
|
||
const costTot = costMix !== null ? costMix * (1 + scrap / 100) + other : (other || null);
|
||
const markup = val('#pvMarkup');
|
||
const suggested = (costTot !== null && markup !== null) ? costTot * (1 + markup / 100) : null;
|
||
|
||
let warn = '';
|
||
if (missing && rows.length) warn += '<div class="alert alert-warning py-2 mb-1"><b>' + missing + ' mescol' + (missing === 1 ? 'a' : 'e') + ' senza g/m o €/kg</b>: il costo è incompleto. Compila i campi in rosso.</div>';
|
||
const checks = data.mix.filter(function(x) {
|
||
return x.check;
|
||
});
|
||
if (checks.length) warn += '<div class="alert alert-warning py-2 mb-1"><b>Prezzo da verificare</b> per ' + checks.map(function(x) {
|
||
return esc(x.name);
|
||
}).join(', ') + ': il prezzo pagato potrebbe non comprendere le materie prime (conto lavoro).</div>';
|
||
$('#pvWarn').html(warn);
|
||
|
||
$('#pvCostMix').text(costMix === null ? '—' : eur(costMix) + '/' + ul());
|
||
$('#pvCostTot').text(costTot === null ? '—' : eur(costTot) + '/' + ul());
|
||
$('#pvSuggested').html(suggested === null ? '—' : eur(suggested) + '<small class="fs-6 text-muted">/' + ul() + '</small>');
|
||
$('#pvSuggestedHint').text(suggested === null ? 'Serve il costo: compila g/m ed €/kg' : 'costo totale × (1 + ' + num(markup, 0) + '%)');
|
||
$('#pvChips .chip-mk').each(function() {
|
||
$(this).toggleClass('active', +$(this).attr('data-v') === markup);
|
||
});
|
||
|
||
if (!offeredTouched) $('#pvOffered').val(suggested === null ? '' : suggested.toFixed(3));
|
||
const offered = val('#pvOffered');
|
||
const mg = (offered && costTot) ? (offered - costTot) / offered * 100 : null;
|
||
$('#pvOfferedInfo').html((offered ? marginPill(mg) : '') + (offeredTouched && suggested ? ' <a href="#" id="pvResetOffered" class="small ms-2">torna al suggerito</a>' : ''));
|
||
|
||
renderRefs(offered);
|
||
renderChart(costTot, offered);
|
||
data._calc = {
|
||
costMix: costMix,
|
||
costTot: costTot,
|
||
suggested: suggested,
|
||
offered: offered,
|
||
rows: rows
|
||
};
|
||
}
|
||
|
||
function renderChips() {
|
||
let html = MARKUPS.map(function(v) {
|
||
return '<button type="button" class="chip-mk" data-v="' + v + '">+' + v + '%</button>';
|
||
}).join('');
|
||
if (data.all.markup !== null && data.all.markup > 0) {
|
||
html += '<button type="button" class="chip-mk" data-v="' + data.all.markup + '" title="Ricarico che si ottiene oggi con il prezzo medio di mercato">come mercato +' + num(data.all.markup, 0) + '%</button>';
|
||
}
|
||
$('#pvChips').html(html);
|
||
// ricarico iniziale: quello di mercato se c'è, altrimenti 100% (se l'utente l'ha già scelto lo tengo)
|
||
if (!markupTouched) $('#pvMarkup').val(data.all.markup !== null && data.all.markup > 0 ? data.all.markup : 100);
|
||
}
|
||
|
||
function refBox(label, v, sub, offered) {
|
||
const parts = [];
|
||
if (v && offered) parts.push('proposto ' + delta(offered, v));
|
||
if (sub) parts.push(sub);
|
||
return '<div class="ref"><div class="l">' + label + '</div><div class="v">' + (v ? eur(v) : '—') + '</div><div class="s">' + parts.join(' · ') + '</div></div>';
|
||
}
|
||
|
||
function renderRefs(offered) {
|
||
const c = data.customer;
|
||
const a = data.all;
|
||
let html = '';
|
||
if (c) {
|
||
html += refBox('Ultimo prezzo al cliente', c.last_price, c.last_date ? 'del ' + fdate(c.last_date) : 'mai venduta a questo cliente', offered);
|
||
html += refBox('Media cliente 12 mesi', c.avg12, '', offered);
|
||
}
|
||
html += refBox('Media mercato 12 mesi', a.avg12, a.n_clients ? a.n_clients + ' clienti' : 'nessuna vendita', offered);
|
||
html += '<div class="ref"><div class="l">Forbice 12 mesi</div><div class="v">' + (a.min12 ? eur(a.min12) + ' – ' + eur(a.max12) : '—') + '</div><div class="s">prezzo minimo e massimo</div></div>';
|
||
$('#pvRefs').html(html);
|
||
}
|
||
|
||
function renderClients() {
|
||
const list = data.all.clients || [];
|
||
if (!list.length) {
|
||
$('#pvClients').html('<div class="text-muted small">Nessuna vendita negli ultimi 12 mesi in questa unità.</div>');
|
||
return;
|
||
}
|
||
const max = Math.max.apply(null, list.map(function(x) {
|
||
return x.price;
|
||
}));
|
||
$('#pvClients').html(list.map(function(x) {
|
||
const me = data.customer && x.code === data.customer.code;
|
||
return '<div class="d-flex justify-content-between align-items-center py-1 border-bottom' + (me ? ' fw-bold' : '') + '">' +
|
||
'<div style="min-width:0;"><div class="text-truncate" style="max-width:220px;">' + (me ? '▶ ' : '') + esc(x.name) + '</div><div class="pv-hint">' + num(x.qty) + ' ' + esc(data.uom) + '</div></div>' +
|
||
'<div class="text-end num"><b>' + eur(x.price) + '</b><div style="height:5px;width:90px;background:#eef2f7;border-radius:9px;margin-top:3px;"><div style="height:5px;border-radius:9px;background:#0f766e;width:' + (x.price / max * 100) + '%"></div></div></div></div>';
|
||
}).join(''));
|
||
}
|
||
|
||
function renderChart(costTot, offered) {
|
||
const ms = data.months;
|
||
const pt = function(map) {
|
||
return ms.map(function(ym) {
|
||
return {
|
||
x: new Date(ym + '-15T12:00:00').getTime(),
|
||
y: map && map[ym] !== undefined ? map[ym] : null
|
||
};
|
||
});
|
||
};
|
||
const series = [{
|
||
name: 'Costo mescola',
|
||
type: 'line',
|
||
data: pt(data.cost_months)
|
||
}, {
|
||
name: 'Media tutti i clienti',
|
||
type: 'line',
|
||
data: pt(data.all.months)
|
||
}];
|
||
if (data.customer) series.push({
|
||
name: data.customer.name,
|
||
type: 'line',
|
||
data: pt(data.customer.months)
|
||
});
|
||
const ann = [];
|
||
if (offered) ann.push({
|
||
y: offered,
|
||
borderColor: '#16a34a',
|
||
strokeDashArray: 0,
|
||
label: {
|
||
text: 'Proposto ' + eur(offered),
|
||
style: {
|
||
background: '#16a34a',
|
||
color: '#fff'
|
||
}
|
||
}
|
||
});
|
||
if (costTot) ann.push({
|
||
y: costTot,
|
||
borderColor: '#dc2626',
|
||
strokeDashArray: 4,
|
||
label: {
|
||
text: 'Costo totale ' + eur(costTot),
|
||
position: 'left',
|
||
textAnchor: 'start',
|
||
style: {
|
||
background: '#dc2626',
|
||
color: '#fff'
|
||
}
|
||
}
|
||
});
|
||
const opts = {
|
||
chart: {
|
||
type: 'line',
|
||
height: 330,
|
||
fontFamily: 'inherit',
|
||
toolbar: {
|
||
show: false
|
||
},
|
||
zoom: {
|
||
enabled: false
|
||
},
|
||
animations: {
|
||
enabled: false
|
||
}
|
||
},
|
||
series: series,
|
||
colors: ['#dc2626', '#475569', '#7c3aed'],
|
||
stroke: {
|
||
width: [2.5, 2.5, 3],
|
||
curve: ['stepline', 'straight', 'straight'],
|
||
dashArray: [6, 0, 0]
|
||
},
|
||
markers: {
|
||
size: [0, 3, 5]
|
||
},
|
||
xaxis: {
|
||
type: 'datetime',
|
||
labels: {
|
||
datetimeUTC: false,
|
||
format: 'MMM yy'
|
||
}
|
||
},
|
||
yaxis: {
|
||
labels: {
|
||
formatter: function(v) {
|
||
return eur(v);
|
||
}
|
||
},
|
||
title: {
|
||
text: '€/' + ul()
|
||
}
|
||
},
|
||
annotations: {
|
||
yaxis: ann
|
||
},
|
||
legend: {
|
||
position: 'top',
|
||
horizontalAlign: 'left'
|
||
},
|
||
grid: {
|
||
borderColor: '#eef2f7'
|
||
},
|
||
tooltip: {
|
||
shared: true,
|
||
x: {
|
||
format: 'MMM yyyy'
|
||
},
|
||
y: {
|
||
formatter: function(v) {
|
||
return v === null ? '—' : eur(v);
|
||
}
|
||
}
|
||
},
|
||
noData: {
|
||
text: 'Nessun dato'
|
||
}
|
||
};
|
||
if (chart) chart.destroy();
|
||
chart = new ApexCharts(document.getElementById('pvChart'), opts);
|
||
chart.render();
|
||
}
|
||
|
||
function renderQuotes() {
|
||
const q = data.quotes || [];
|
||
if (!q.length) {
|
||
$('#pvQuotes').html('<tr><td colspan="8" class="text-muted">Nessun preventivo salvato per questa matrice.</td></tr>');
|
||
return;
|
||
}
|
||
$('#pvQuotes').html(q.map(function(x, i) {
|
||
const u = x.uom === 'KG' ? 'kg' : 'm';
|
||
return '<tr>' +
|
||
'<td class="num">' + fdate(x.created_at) + ' ' + String(x.created_at).substring(11, 16) + '</td>' +
|
||
'<td>' + esc(x.customer_name || '—') + '</td>' +
|
||
'<td class="text-end num">' + eur(+x.cost_total) + '/' + u + '</td>' +
|
||
'<td class="text-end num">' + (x.markup_pct === null ? '—' : num(+x.markup_pct, 0) + '%') + '</td>' +
|
||
'<td class="text-end num"><b>' + eur(+x.price_offered) + '</b>/' + u + '</td>' +
|
||
'<td style="max-width:320px;">' + esc(x.comment || '') + '</td>' +
|
||
'<td>' + esc(x.user_name || x.username || '') + '</td>' +
|
||
'<td class="text-nowrap no-print"><button type="button" class="btn btn-sm btn-outline-secondary pv-reopen" data-i="' + i + '">Riapri</button> ' +
|
||
'<button type="button" class="btn btn-sm btn-outline-danger pv-del" data-id="' + x.id + '" title="Elimina">🗑️</button></td>' +
|
||
'</tr>';
|
||
}).join(''));
|
||
}
|
||
|
||
function reopen(x) {
|
||
const go = function() {
|
||
$('#pvScrap').val(+x.scrap_pct || 0);
|
||
$('#pvOther').val(+x.other_costs || 0);
|
||
if (x.markup_pct !== null) {
|
||
markupTouched = true;
|
||
$('#pvMarkup').val(+x.markup_pct);
|
||
}
|
||
$('#pvComment').val(x.comment || '');
|
||
let mix = [];
|
||
try {
|
||
mix = JSON.parse(x.mix_detail || '[]');
|
||
} catch (e) {}
|
||
$('#pvMixBody tr[data-i]').each(function() {
|
||
const m = data.mix[+$(this).attr('data-i')];
|
||
const s = mix.find(function(y) {
|
||
return y.id === m.id;
|
||
});
|
||
if (s && s.edited) {
|
||
$(this).find('.pv-gm').val(s.gm === null ? '' : s.gm);
|
||
$(this).find('.pv-eur').val(s.eur_kg === null ? '' : s.eur_kg);
|
||
}
|
||
});
|
||
offeredTouched = true;
|
||
$('#pvOffered').val((+x.price_offered).toFixed(3));
|
||
calc();
|
||
window.scrollTo({
|
||
top: 0,
|
||
behavior: 'smooth'
|
||
});
|
||
};
|
||
const kind = x.uom === 'KG' ? 'kg' : 'm';
|
||
if (kind !== state.u || (x.customer_code || '') !== (state.c || '')) {
|
||
state.u = kind;
|
||
state.c = x.customer_code || '';
|
||
setCustomer(state.c, x.customer_name);
|
||
load();
|
||
const t = setInterval(function() {
|
||
if (data && data.uom === x.uom) {
|
||
clearInterval(t);
|
||
go();
|
||
}
|
||
}, 150);
|
||
setTimeout(function() {
|
||
clearInterval(t);
|
||
}, 8000);
|
||
} else go();
|
||
}
|
||
|
||
function save() {
|
||
const c = data && data._calc;
|
||
if (!c || !c.offered) {
|
||
Swal.fire({
|
||
icon: 'warning',
|
||
title: 'Manca il prezzo',
|
||
text: 'Compila il costo e il ricarico, oppure il prezzo proposto.'
|
||
});
|
||
return;
|
||
}
|
||
const body = new URLSearchParams({
|
||
action: 'save',
|
||
idmatrice: state.m,
|
||
kind: state.u,
|
||
customer: state.c || '',
|
||
scrap_pct: val('#pvScrap') || 0,
|
||
other_costs: val('#pvOther') || 0,
|
||
markup_pct: val('#pvMarkup') === null ? '' : val('#pvMarkup'),
|
||
price_offered: c.offered,
|
||
ref_last_price: data.customer && data.customer.last_price ? data.customer.last_price : '',
|
||
ref_avg_price: data.all.avg12 || '',
|
||
comment: $('#pvComment').val(),
|
||
mix: JSON.stringify(c.rows.map(function(r) {
|
||
return {
|
||
id: r.id,
|
||
name: r.name,
|
||
gm: r.gm,
|
||
eur_kg: r.eur_kg,
|
||
edited: r.edited
|
||
};
|
||
}))
|
||
});
|
||
fetch('sales_preventivo.php', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded'
|
||
},
|
||
body: body.toString()
|
||
})
|
||
.then(function(r) {
|
||
return r.json();
|
||
}).then(function(d) {
|
||
if (!d.success) {
|
||
Swal.fire({
|
||
icon: 'error',
|
||
title: 'Non salvato',
|
||
text: d.message || 'Errore'
|
||
});
|
||
return;
|
||
}
|
||
Swal.fire({
|
||
icon: 'success',
|
||
title: 'Preventivo salvato',
|
||
timer: 1300,
|
||
showConfirmButton: false
|
||
});
|
||
const qs = new URLSearchParams({
|
||
action: 'data',
|
||
m: state.m,
|
||
c: state.c || '',
|
||
u: state.u
|
||
});
|
||
fetch('?' + qs).then(function(r) {
|
||
return r.json();
|
||
}).then(function(dd) {
|
||
if (dd.success) {
|
||
data.quotes = dd.quotes;
|
||
renderQuotes();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function setCustomer(code, name) {
|
||
const $s = $('#pvCustomer');
|
||
if (code && !$s.find('option[value="' + code + '"]').length) $s.append(new Option(name || code, code, true, true));
|
||
$s.val(code || null).trigger('change.select2');
|
||
}
|
||
|
||
/* ---------- eventi ---------- */
|
||
$(function() {
|
||
readUrl();
|
||
$('#pvMatrice').select2({
|
||
theme: 'bootstrap-5',
|
||
placeholder: 'Cerca la matrice (es. 00786S)…',
|
||
allowClear: true,
|
||
width: '100%',
|
||
minimumInputLength: 0,
|
||
ajax: {
|
||
url: 'sales_preventivo.php',
|
||
delay: 250,
|
||
data: function(p) {
|
||
return {
|
||
action: 'matrici',
|
||
q: p.term || ''
|
||
};
|
||
},
|
||
processResults: function(d) {
|
||
return {
|
||
results: d.results || []
|
||
};
|
||
}
|
||
},
|
||
templateResult: function(r) {
|
||
if (!r.id) return r.text;
|
||
const img = r.photo ? '<img src="' + esc(r.photo) + '" onerror="this.outerHTML=\'<span class="ph"></span>\'">' : '<span class="ph"></span>';
|
||
return $('<div class="mx-opt">' + img + '<div><b>' + esc(r.text) + '</b>' + (r.has_gm ? '' : ' <span class="tb tb-warn">senza g/m</span>') + '</div></div>');
|
||
}
|
||
}).on('change', function() {
|
||
state.m = parseInt($(this).val(), 10) || null;
|
||
load();
|
||
});
|
||
|
||
fetch('?action=customers').then(function(r) {
|
||
return r.json();
|
||
}).then(function(d) {
|
||
const $s = $('#pvCustomer');
|
||
$s.append(new Option('', '', false, false));
|
||
(d.customers || []).forEach(function(c) {
|
||
$s.append(new Option(c.name + ' (' + c.code + ')', c.code, false, c.code === state.c));
|
||
});
|
||
$s.select2({
|
||
theme: 'bootstrap-5',
|
||
placeholder: 'Nessun cliente: prezzo di mercato',
|
||
allowClear: true,
|
||
width: '100%'
|
||
})
|
||
.on('change', function() {
|
||
const v = $(this).val() || '';
|
||
if (v === (state.c || '')) return;
|
||
state.c = v;
|
||
load();
|
||
});
|
||
});
|
||
|
||
$('#pvUom').on('click', 'button', function() {
|
||
state.u = $(this).attr('data-u');
|
||
load();
|
||
});
|
||
$('#pvMixBody').on('input', 'input', calc);
|
||
$('#pvScrap, #pvOther').on('input', calc);
|
||
$('#pvMarkup').on('input', function() {
|
||
markupTouched = true;
|
||
calc();
|
||
});
|
||
$('#pvOffered').on('input', function() {
|
||
offeredTouched = true;
|
||
calc();
|
||
});
|
||
$('#pvOfferedInfo').on('click', '#pvResetOffered', function(e) {
|
||
e.preventDefault();
|
||
offeredTouched = false;
|
||
calc();
|
||
});
|
||
$('#pvChips').on('click', '.chip-mk', function() {
|
||
markupTouched = true;
|
||
$('#pvMarkup').val($(this).attr('data-v'));
|
||
calc();
|
||
});
|
||
$('#pvSave').on('click', save);
|
||
$('#pvPrint').on('click', function() {
|
||
window.print();
|
||
});
|
||
$('#pvQuotes').on('click', '.pv-reopen', function() {
|
||
reopen(data.quotes[+$(this).attr('data-i')]);
|
||
}).on('click', '.pv-del', function() {
|
||
const id = $(this).attr('data-id');
|
||
Swal.fire({
|
||
title: 'Eliminare il preventivo?',
|
||
icon: 'warning',
|
||
showCancelButton: true,
|
||
confirmButtonText: 'Sì, elimina',
|
||
cancelButtonText: 'Annulla',
|
||
confirmButtonColor: '#d33'
|
||
})
|
||
.then(function(res) {
|
||
if (!res.isConfirmed) return;
|
||
fetch('sales_preventivo.php', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded'
|
||
},
|
||
body: 'action=delete&id=' + encodeURIComponent(id)
|
||
})
|
||
.then(function(r) {
|
||
return r.json();
|
||
}).then(function() {
|
||
data.quotes = data.quotes.filter(function(x) {
|
||
return String(x.id) !== String(id);
|
||
});
|
||
renderQuotes();
|
||
});
|
||
});
|
||
});
|
||
|
||
// matrice arrivata dall'URL (es. dalla pagina Matrici): la mostro nella tendina
|
||
if (state.m) {
|
||
load();
|
||
} else load();
|
||
});
|
||
})();
|
||
</script>
|
||
<script src="sales_help.js?v=6"></script>
|
||
</body>
|
||
|
||
</html>
|