{% extends 'base.html.twig' %}
{% block title %}
{% trans %}
sales
{% endtrans %}
{% endblock %}
{% block head %}
<style>
/* Keep all form elements at 12px on this page */
select, textarea, button { font-size: 12px !important; }
.form-control, .custom-select { font-size: 12px !important; }
/* Select2 text sizes */
.select2-container .select2-selection__rendered,
.select2-results__option,
.select2-container .select2-search__field { font-size: 12px !important; }
#adder-allocated-quantity,
#adder-quantity-unit {
vertical-align: middle;
}
.product-select-cell {
max-width: 250px;
/* Hücrenin alabileceği maksimum genişlik. İstediğiniz gibi ayarlayın. */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Ekleme Satırı Stilleri */
#adder-row {
background-color: #f8f9fa;
/* Hafif gri arkaplan */
}
#adder-row td {
vertical-align: middle;
padding: 0.25rem; /* Boşluk azaltıldı */
border-top: 2px dashed #e3e6f0;
}
/* Adder satırında hücreler arası yatay boşluğu minimuma indir */
#adder-row .input-group { margin-right: 4px; }
#adder-row .btn-icon, #adder-row .btn { margin-right: 4px; }
/* Yeni Card Header Stili */
.bg-light-blue {
background-color: #f0f3ff !important;
/* Hafif mavi arkaplan */
}
#product-list-table-tbody input {
/*border: none;*/
/*padding: 0;*/
/*margin: 0;*/
background-color: white;
}
.info-span {
color: red;
font-weight: bold;
}
.cost-detail-button {
color: blue;
font-width: bold;
border: none;
background-color: transparent;
}
.attention-icon {
display: inline-block;
vertical-align: middle;
margin-left: 5px;
font-size: 16px;
color: red;
font-weight: bold;
}
.quantity-input-added {
width: 60px;
}
.select2-container {
width: 100% !important;
/* Genişlik uyumluluğu için */
}
/* En kritik CSS düzeltmesi olmasa da, z-index çakışmalarına karşı bir güvencedir.
Asıl çözüm JavaScript'tedir. */
.select2-container--open {
z-index: 1056 !important;
}
.select2-container {
width: 100% !important;
}
.select2-container--open {
z-index: 1056 !important;
}
.select2-container--bootstrap4 .select2-selection--single
{
padding: .50rem !important;
}
.select2-container .select2-selection--single {
height: 28px !important;
}
/* === SEÇİM KARTLARI İÇİN CSS === */
.selection-card-label {
display: block;
cursor: pointer;
width: 100%;
}
.selection-card {
border: 2px solid #e3e6f0;
border-radius: 0.5rem;
padding: 0.50rem;
transition: all 0.2s ease-in-out;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
/* Kartın üzerine gelindiğinde (hover) */
.selection-card:hover {
border-color: #4e73df;
transform: translateY(-2px);
/* Hafif yukarı kalkma efekti */
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
/* Kart içindeki başlık (Fatura, Proforma) */
.selection-title {
font-size: 1.1rem;
font-weight: 500;
color: #5a5c69;
transition: color 0.2s ease-in-out;
}
/* Onay işareti (başlangıçta gizli) */
.checkmark {
font-size: 1.25rem;
color: #4e73df;
opacity: 0;
transform: scale(0.5);
transition: all 0.2s ease-out;
margin-left: 0.75rem;
}
/* SEÇİM YAPILDIĞINDAKİ STİLLER */
/* CSS :has() seçicisi ile, içindeki radio input seçili olan etiketin kartını stillendiriyoruz. */
.selection-card-label:has(input[type="radio"]:checked) .selection-card {
border-color: #4e73df;
background-color: #f0f3ff;
}
/* Seçili kartın başlık rengini değiştir */
.selection-card-label:has(input[type="radio"]:checked) .selection-title {
color: #4e73df;
font-weight: 600;
}
/* Seçili kartın onay işaretini görünür yap */
.selection-card-label:has(input[type="radio"]:checked) .checkmark {
opacity: 1;
transform: scale(1);
}
/* Bilgi satırı için stil */
#info-details-content {
background-color: #f1faff;
/* Hafif mavi arka plan */
border-bottom: 2px dashed #e3e6f0;
/* Altına kesikli çizgi ekler */
}
.info-details-content-class {
background-color: #f1faff;
/* Hafif mavi arka plan */
border-bottom: 2px dashed #e3e6f0;
/* Altına kesikli çizgi ekler */
}
/* Make product table full-width and remove right gaps */
.card-body > .table-responsive { padding-right: 0; }
#product-list-table { width: 100% !important; table-layout: fixed; }
#product-list-table th, #product-list-table td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#product-list-table .product-select-cell select { width: 100%; }
/* Adjust specific column widths to fit viewport */
.summary-row td {
border-top: none;
background-color: #f8f9fc;
padding: 0;
}
.sale-summary-row {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 12px;
padding: 0.35rem 0.75rem;
border-radius: 0.35rem;
margin-right: 1.5rem;
}
.sale-summary-row .summary-label {
font-weight: 600;
color: #4b5563;
white-space: nowrap;
}
.sale-summary-row .summary-value {
font-weight: 700;
color: #111827;
text-align: right;
min-width: 120px;
white-space: nowrap;
}
.sale-summary-row.summary-tax .summary-label,
.sale-summary-row.summary-tax .summary-value {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.sale-summary-row.summary-tax .summary-label div,
.sale-summary-row.summary-tax .summary-value div {
white-space: nowrap;
}
.sale-summary-row.summary-total .summary-value {
font-size: 1.25rem;
}
#product-list-table th:nth-child(1), #product-list-table td:nth-child(1) { width: 5%; }
#product-list-table th:nth-child(2), #product-list-table td:nth-child(2) { width: 25%; }
#product-list-table th:nth-child(3), #product-list-table td:nth-child(3) { width: 10%; }
#product-list-table th:nth-child(4), #product-list-table td:nth-child(4) { width: 10%; }
#product-list-table th:nth-child(5), #product-list-table td:nth-child(5) { width: 10%; }
#product-list-table th:nth-child(6), #product-list-table td:nth-child(6) { width: 8%; }
#product-list-table th:nth-child(7), #product-list-table td:nth-child(7) { width: 8%; }
#product-list-table th:nth-child(8), #product-list-table td:nth-child(8) { width: 15%; }
#product-list-table th:nth-child(9), #product-list-table td:nth-child(9) { width: 9%; }
</style>
{% endblock %}
{% block body %}
<div class="container-fluid m-0 p-0">
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<div class="btn-group" role="group">
<div class="d-md-none">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% trans %} actions {% endtrans %}
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<button type="button" class="dropdown-item" data-toggle="modal" data-target="#payments-modal">
<i class="fas fa-money-check-alt mr-1"></i>
{% trans %}payments{% endtrans %}
</button>
<button type="button" data-toggle="modal" data-target="#add-customer-modal" class="dropdown-item">
<i class="fas fa-user-plus mr-1"></i>
{% trans %}addCustomer{% endtrans %}
</button>
<button type="button" class="dropdown-item js-edit-customer">
<i class="fas fa-user-edit mr-1"></i>
{% trans %}editCustomer{% endtrans %}
</button>
<button type="button" class="dropdown-item" id="btn-gift-voucher-mobile">
<i class="fas fa-gift mr-1"></i>
{{ 'giftVoucher'|trans }}
</button>
</div>
</div>
</div>
<div class="d-none d-md-block">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary" data-toggle="modal" data-target="#payments-modal">
<i class="fas fa-money-check-alt mr-1"></i>
{% trans %}payments{% endtrans %}
</button>
<button type="button" data-toggle="modal" data-target="#add-customer-modal" class="btn btn-outline-secondary">
<i class="fas fa-user-plus mr-1"></i>
{% trans %}addCustomer{% endtrans %}
</button>
<button type="button" class="btn btn-outline-secondary js-edit-customer">
{% trans %}editCustomer{% endtrans %}
</button>
<button type="button" class="btn btn-outline-secondary" id="btn-gift-voucher" title="{{ 'useGiftVoucher'|trans }}">
<i class="fas fa-gift mr-1"></i>
{{ 'giftVoucher'|trans }}
</button>
</div>
</div>
</div>
<button type="button" id="submitbtn" class="btn btn-success">
<i class="fas fa-save mr-1"></i>
{% trans %}
save
{% endtrans %}
</button>
</div>
<div class="card-body">
{# FATURA VE PROFORMA SEÇENEKLERİ #}
<div class="form-group">
<label class="form-label font-weight-bold">{{ 'salesType'|trans }}</label>
<div class="row">
{# Fatura Seçeneği #}
<div class="col-md-6">
<label class="selection-card-label">
<input type="radio" name="salesType" class="d-none" value="invoice">
<div class="selection-card">
<span class="selection-title">{% trans %}invoice{% endtrans %}</span>
<i class="fas fa-check-circle checkmark"></i>
</div>
</label>
</div>
{# Proforma Seçeneği #}
<div class="col-md-6">
<label class="selection-card-label">
<input type="radio" name="salesType" class="d-none" value="proforma" checked>
<div class="selection-card">
<span class="selection-title">{% trans %}proforma{% endtrans %}</span>
<i class="fas fa-check-circle checkmark"></i>
</div>
</label>
</div>
</div>
</div>
<div style="margin-bottom: 10px">
{# SATIŞ FORMU başlangıç #}
{{ form_start(form) }}
<div class="">
<div class="row">
<div class="col-lg-6 col-md-12">
{{ form_row(form.totalPurchasePrice) }}
{{ form_row(form.status) }}
</div>
<div class="col-lg-6 col-md-12">
{{ form_row(form.customer) }}
{{ form_row(form.seller) }}
</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-6">{{ form_row(form.salesDate) }}</div>
<div class="col-lg-3 col-md-6">{{ form_row(form.deliveryDate) }}</div>
<div class="col-lg-3 col-md-6">{{ form_row(form.dueDate) }}</div>
<div class="col-lg-3 col-md-6">{{ form_row(form.validDate) }}</div>
</div>
</div>
{# SATIŞ FORMU bitiş #}
{# Ödemeler Tablosu modal #}
<div class="modal fade" id="payments-modal" role="dialog" aria-labelledby="paymentsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="paymentsModalLabel">{% trans %}payments{% endtrans %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="card mb-1">
<div class="card-header d-flex align-items-center justify-content-between">
<div>
<h5 class="mb-0">{% trans %}payments{% endtrans %}</h5>
<div style="float: left; width: 250px">
{% trans %}total{% endtrans %}:
<span id="total-amount-span"></span>
{% trans %}remainingAmount{% endtrans %}:
<span id="remainder-amount-span"></span>
</div>
</div>
<button class="btn btn-primary" type="button" id="btn-addPayment" data-toggle="modal" data-target="#add-payment-modal">
{% trans %}addPayment{% endtrans %}
</button>
</div>
<div class="card-body">
<div>
<table class="table" id="payment-list-table">
<thead>
<tr>
<th>{% trans %}amount{% endtrans %}</th>
<th>{% trans %}status{% endtrans %}</th>
<th>{% trans %}paymentDate{% endtrans %}</th>
<th>{% trans %}dueDate{% endtrans %}</th>
<th>{% trans %}paymentMethod{% endtrans %}</th>
<th>{% trans %}description{% endtrans %}</th>
<th> </th>
</tr>
</thead>
<tbody id="payment-list-table-tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-light-blue border-0 py-3">
<div class="row align-items-center">
<div class="col-md-4">
<label for="warehouse-select" class="font-weight-bold mb-1">{% trans %}warehouse{% endtrans %}</label>
<select id="warehouseselect" class="form-control">
<option value="">{% trans %}selectwarehouse{% endtrans %}</option>
{% for warehouse in warehouses %}
<option value="{{ warehouse.id }}">{{ warehouse.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-5"></div>
<div id="product-info-box" class="col-md-3" style="display: none;">
<div class="info-box-content">
<div>
{% trans %}stockQuantity{% endtrans %}:
<strong id="info-stock"></strong>
</div>
<div>
{% trans %}averageCost{% endtrans %}:
<strong id="info-cost"></strong>
</div>
</div>
</div>
</div>
</div>
<div class="card-body" style="padding: 0">
<div class="table-responsive">
<table class="table modern-table" id="product-list-table" style="width: 100%;">
<thead>
<tr>
<th style="width: 5%;">{% trans %}info{% endtrans %}</th>
<th style="width: 20%;">{% trans %}product{% endtrans %}</th>
<th style="width: 20%;">{% trans %}quantity{% endtrans %}</th>
<th style="width: 15%;">{% trans %}unitPrice{% endtrans %}</th>
<th style="width: 10%;">{% trans %}discount{% endtrans %} (%)</th>
<th style="width: 10%;">TVA (%)</th>
<th style="width: 15%; text-align: left !important;" class="text-right">{% trans %}total{% endtrans %}</th>
<th style="width: 15%;" class="text-center">{% trans %}action{% endtrans %}</th>
</tr>
</thead>
<tbody id="product-list-table-tbody">
</tbody>
<tbody id="adder-body">
<tr id="adder-row">
<td>
<button type="button" id="show-info-btn"
class="btn btn-sm btn-info btn-icon" title="{{ 'productInfo'|trans }}">
<i class="fas fa-info-circle"></i>
</button>
</td>
<td class="product-select-cell">
<select id="productselect" class="form form-control">
<option value="">{{ 'selectWarehouseFirst'|trans }}</option>
</select>
</td>
<td>
<div class="input-group input-group-sm" style="max-width: 220px;">
<input type="text" id="adder-allocated-quantity" class="form-control" value="0" min="0">
<div class="input-group-append">
<select id="adder-quantity-unit-select" class="custom-select custom-select-sm">
<option value="">-</option>
</select>
</div>
</div>
</td>
<td><input type="text" id="adder-price" value="0" min="0"
class="form-control form-control-sm price-mask" placeholder="Fiyat">
</td>
<td>
<select name="adder-discount-select" id="adder-discount-select"
class="form-control form-control-sm">
<option value="0">% 0</option>
<option value="1">% 1</option>
<option value="2">% 2</option>
<option value="3">% 3</option>
<option value="4">% 4</option>
<option value="5">% 5</option>
<option value="20">% 20</option>
<option value="30">% 30</option>
<option value="100">% 100</option>
</select>
</td>
<td>
<select name="adder-tva-select" id="adder-tva-select"
class="form-control form-control-sm">
<option value="0" disabled>% 0</option>
<option value="10">% 10</option>
<option value="20">% 20</option>
</select>
</td>
<td class="text-right font-weight-bold small">
<input name="adder-total" id="adder-total" type="number"
class="form-control form-control-sm"
placeholder="{% trans %} total {% endtrans %}"/>
</td>
<td class="text-center">
<div class="btn-group" role="group">
<button type="button" id="open-unallocated-modal" class="btn btn-outline-secondary btn-sm" title="{% trans %}unAllocatedQuantity{% endtrans %}">
<i class="fas fa-box-open"></i>
<span id="unalloc-display" class="badge badge-light ml-1">0</span>
</button>
<button type="button" id="add-product-row-btn"
class="btn btn-sm btn-success btn-icon">
<i class="fas fa-plus"></i>
</button>
</div>
<input type="number" id="adder-unallocated-quantity" class="form-control d-none" value="0" min="0">
<select id="adder-unallocated-quantity-unit-select" class="custom-select custom-select-sm d-none">
<option value="">-</option>
</select>
</td>
</tr>
<tr id="info-details-row">
<td colspan="8" style="padding: 0; border-top: none;">
<div id="info-details-content" class="p-3" style="display: none;">
</div>
</td>
</tr>
</tbody>
<tfoot>
<!-- Rows will be injected by updateTotalAmounts() -->
<tr class="summary-row-placeholder">
<td colspan="8"></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
{{ form_end(form) }}
</div>
</div>
</div>
</div>
<div class="modal fade" id="add-customer-modal" role="dialog" aria-labelledby="customerModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="customerModalLabel">{% trans %}addCustomer{% endtrans %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
{{ form_start(customerForm) }}
<div class="modal-body">
{{ form_widget(customerForm) }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{% trans %}cancel{% endtrans %}</button>
<button type="submit" id="btn-customer-save" class="btn btn-primary">{% trans %}save{% endtrans %}</button>
</div>
{{ form_end(customerForm) }}
</div>
</div>
</div>
<div class="modal fade" id="unallocatedModal" tabindex="-1" role="dialog" aria-labelledby="unallocatedModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="unallocatedModalLabel">{% trans %}unAllocatedQuantity{% endtrans %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="modal-unallocated-quantity">{% trans %}quantity{% endtrans %}</label>
<input type="text" id="modal-unallocated-quantity" class="form-control mask-money" value="0">
</div>
<div class="form-group">
<label for="modal-unallocated-unit">{% trans %}unit{% endtrans %}</label>
<select id="modal-unallocated-unit" class="custom-select">
<option value="">-</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{% trans %}close{% endtrans %}</button>
<button type="button" id="save-unallocated-btn" class="btn btn-primary">{% trans %}save{% endtrans %}</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="add-payment-modal" role="dialog" aria-labelledby="paymentModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="paymentModalLabel">{% trans %}addPayment{% endtrans %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
{{ form_widget(paymentForm) }}
{{ form_start(paymentForm) }}
{{ form_end(paymentForm) }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{% trans %}cancel{% endtrans %}</button>
<button type="button" id="save-payment-btn" class="btn btn-primary">{% trans %}save{% endtrans %}</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="updateCustomerModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="updateModalLabel">{% trans %}updateCustomerInfoMessage{% endtrans %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<form id="updateCustomerForm">
<div class="modal-body">
<input type="hidden" id="update_customer_id">
<div class="form-group">
<label for="update_customer_fullName">{% trans %}fullName{% endtrans %}</label>
<input type="text" class="form-control" id="update_customer_fullName" required>
</div>
<div class="form-group">
<label for="update_customer_email">{% trans %}email{% endtrans %}</label>
<input type="email" class="form-control" id="update_customer_email">
</div>
<div class="form-group">
<label for="update_customer_phone">{% trans %}phone{% endtrans %}</label>
<input type="text" class="form-control" id="update_customer_phone">
</div>
<div class="form-group">
<label for="update_customer_address">{% trans %}address{% endtrans %}</label>
<textarea class="form-control" id="update_customer_address" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{% trans %}close{% endtrans %}</button>
<button type="submit" class="btn btn-primary">{% trans %}save{% endtrans %}</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="updateServiceModal" tabindex="-1" role="dialog" aria-labelledby="updateServiceModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="updateServiceModalLabel">{% trans %}updateServiceInfoMessage{% endtrans %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<form id="updateServiceForm" onsubmit="return false;">
<div class="modal-body">
<input type="hidden" id="update_service_id">
<div class="form-group">
<label for="update_service_name">{% trans %}serviceName{% endtrans %}</label>
<input type="text" class="form-control" id="update_service_name">
</div>
<div class="form-group">
<label for="update_service_cost">{% trans %}cost{% endtrans %}</label>
<input type="text" value="0.00" class="form-control mask-money" id="update_service_cost" required>
</div>
<div class="form-group">
<label for="update_service_description">{% trans %}description{% endtrans %}</label>
<input type="text" class="form-control" id="update_service_description">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{% trans %}close{% endtrans %}</button>
<button type="button" class="btn btn-primary" id="service_modal_save_changes_btn">{% trans %}save{% endtrans %}</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="autoPaymentModal" tabindex="-1" role="dialog" aria-labelledby="autoPaymentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="autoPaymentModalLabel">{{ 'autoPaymentConfirmation'|trans }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p id="auto-payment-message" class="mb-3"></p>
<div class="border rounded px-3 py-2 bg-light">
<div class="d-flex justify-content-between"><span>{{ 'salesTotal'|trans }}</span><span id="auto-payment-sale-total">0,00 {{ 'currency.symbol'|trans }}</span></div>
<div class="d-flex justify-content-between"><span>{{ 'currentPayments'|trans }}</span><span id="auto-payment-current-payments">0,00 {{ 'currency.symbol'|trans }}</span></div>
<div class="d-flex justify-content-between font-weight-bold"><span>{{ 'paymentToAdd'|trans }}</span><span id="auto-payment-amount">0,00 {{ 'currency.symbol'|trans }}</span></div>
<hr>
<div class="small">
<div><strong>{{ 'paymentStatusLabel'|trans }}:</strong> <span id="auto-payment-status"></span></div>
<div><strong>{{ 'paymentTypeLabel'|trans }}:</strong> <span id="auto-payment-method"></span></div>
<div><strong>{{ 'description'|trans }}:</strong> <span id="auto-payment-description"></span></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ 'cancel'|trans }}</button>
<button type="button" class="btn btn-primary" id="confirm-auto-payment">{{ 'confirmAction'|trans }}</button>
</div>
</div>
</div>
</div>
<!-- Hediye Çeki Kullanım Modalı -->
<div class="modal fade" id="gift-voucher-usage-modal" tabindex="-1" role="dialog" aria-labelledby="giftVoucherUsageModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="giftVoucherUsageModalLabel">{{ 'giftVoucherUsage'|trans }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<strong><span id="voucher-customer-name"></span></strong>{{ 'voucherBalanceInfoSuffix'|trans }}: <strong id="voucher-total-balance">0.00 TL</strong>
</div>
<div class="form-group">
<label for="voucher-use-amount">{{ 'amountToUse'|trans }}</label>
<input type="number" id="voucher-use-amount" class="form-control" placeholder="0.00" min="0" step="0.01">
<small class="form-text text-muted">{{ 'enterAmountToUse'|trans }}</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ 'cancel'|trans }}</button>
<button type="button" class="btn btn-primary" id="btn-confirm-voucher-usage">{{ 'confirmAndAdd'|trans }}</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascript %}
<script src="{{ asset('/decimal.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const saleDateInput = document.getElementById('sales_form_salesDate');
if (saleDateInput && !saleDateInput.value) {
const now = new Date();
const localDate = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().split('T')[0];
saleDateInput.value = localDate;
saleDateInput.dispatchEvent(new Event('change', { bubbles: true }));
}
});
$(document).ready(function() {
applyMasks();
const isDuplicatedProforma = {{ sales is defined and sales.id is not null ? 'true' : 'false' }};
const duplicatedSoldItems = (function(){
if(!isDuplicatedProforma){ return []; }
const items = [];
{% if sales is defined and sales.productsSolds is defined %}
{% for ps in sales.productsSolds %}
items.push({
productid: {{ ps.product ? ps.product.id : 0 }},
productName: '{{ ps.productName|default(ps.product ? ps.product.name : '')|e('js') }}',
code: '{{ ps.product ? ps.product.code|e('js') : '' }}',
measurement: '{{ ps.measurement is not null ? ps.measurement|e('js') : (ps.product and ps.product.measurement ? ps.product.measurement.name|e('js') : '') }}',
quantity: {{ ps.quantity is not null ? ps.quantity|number_format(2, '.', '') : '0' }},
unAllocatedQuantity: {{ ps.unAllocatedQuantity is not null ? ps.unAllocatedQuantity|number_format(2, '.', '') : '0' }},
unitPrice: {{ ps.totalUnitPurchasePrice is not null ? ps.totalUnitPurchasePrice|number_format(2, '.', '') : '0' }},
tax: {{ ps.tax is not null ? ps.tax|number_format(2, '.', '') : '0' }},
discount: {{ ps.discount is not null ? ps.discount|number_format(2, '.', '') : '0' }},
totalPurchasePrice: {{ ps.totalPuchasePrice is not null ? ps.totalPuchasePrice|number_format(2, '.', '') : (ps.totalPuchasePrice is not null ? ps.totalPuchasePrice|number_format(2, '.', '') : (ps.totalPrice is not null ? ps.totalPrice|number_format(2, '.', '') : '0')) }},
totalPrice: {{ ps.totalPrice }},
warehouse: {{ sales.warehouse ? sales.warehouse.id : 0 }},
thickness: 0,
width: 0,
height: 0,
cost: 0,
productType: '{{ ps.product and ps.product.productTypeEnum ? ps.product.productTypeEnum.name|e('js') : '' }}',
selectedUnitId: {{ ps.selectedUnit ? ps.selectedUnit.id : 'null' }}
});
{% endfor %}
{% endif %}
return items;
})();
// TODO: Duplicated items
async function importDuplicatedSoldItemsIfNeeded(){
await getProductsFromWarehouse();
if(!isDuplicatedProforma || duplicatedSoldItems.length === 0){
return;
}
const targetWarehouseId = {{ sales is defined and sales.warehouse ? sales.warehouse.id : 0 }};
if(targetWarehouseId){
$('#warehouseselect').val(String(targetWarehouseId));
$('#warehouseselect').trigger('change');
}
const start = Date.now();
const timeoutMs = 15000;
const timer = setInterval(function(){
if(Array.isArray(products) && products.length > 0){
clearInterval(timer);
try{
duplicatedSoldItems.forEach(function(item){
const p = products.find(function(prod){ return prod.productid === item.productid; });
if(p){
selectProduct(p.productid);
}
addProductToSoldList(
item.productid,
item.productName,
item.code,
item.measurement,
item.quantity,
item.unitPrice,
item.tax,
item.discount,
item.totalPurchasePrice,
item.warehouse,
item.unAllocatedQuantity,
item.thickness,
item.width,
item.height,
item.cost,
item.productType,
item.selectedUnitId
);
});
fetchSoldListRows();
updateTotalAmounts();
$('#sales_form_totalPurchasePrice').val(addCommas(getTotalSoldAmount()));
fetchPaymentForm();
fetchStocks();
}catch(e){
console.error('Duplicate import error', e);
}
}else if(Date.now() - start > timeoutMs){
clearInterval(timer);
console.warn('Timed out waiting for products to load');
}
}, 200);
}
if(isDuplicatedProforma){
importDuplicatedSoldItemsIfNeeded();
}
// =============================================================================
// INITIALIZATION & GLOBAL VARIABLES
// =============================================================================
// Collapse sidebar only on this page
$('body').addClass('sidebar-toggled');
$('#accordionSidebar').addClass('toggled');
// Product data structure
let product = {
code: "",
id: 0,
length: 0,
measurement: "",
name: "",
priceFob: 0,
priceNavlun: 0,
productid: 0,
purchaseTotalAmount: 0,
quantity: 0,
thickness: 0,
totalQuantity: 0,
totalUnitPrice: 0,
width: 0,
cost: 0,
measurementUnit: "",
stock: 0,
warehouseid: 0,
selected: false,
}
let products = [];
// Sold product data structure
let soldProduct = {
productid: 0,
productName: "",
code: "",
measurement: "",
quantity: 0,
baseUnitQuantity: 0,
unAllocatedQuantity: 0,
unitPrice: 0,
tax: 0,
discount: 0,
totalPurchasePrice: 0,
warehouse: 0,
}
let soldList = [];
const MONEY_ROUNDING_MODE = Decimal.ROUND_HALF_UP;
// Payment data structure
let payment = {
uuid: 0,
id: 0,
amount: 0,
status: 0,
paymentDate: new Date(),
paymentDueDate: new Date(),
paymentMethod: 0,
description: 0,
}
let payments = []
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
function formatCurrency(amount) {
if (amount instanceof Decimal) {
amount = amount.toNumber();
}
return addCommas(amount.toFixed(2)) + ' {{ 'currency.symbol'|trans }}';
}
// Convert entered quantity in selected unit to product base unit quantity
function computeBaseUnitQuantity(productId, selectedUnitId, quantity, onDone){
try{
const product = products.find(p => p.productid === productId);
if(!product){ onDone(quantity); return; }
// If unit not selected or already base unit, fallback to same quantity
if(!selectedUnitId || String(selectedUnitId) === '' || String(selectedUnitId) === String(product.measurementUnitId)){
onDone(quantity);
return;
}
$.ajax({
url: '/admin/measurement/convert-to-product-unit',
method: 'POST',
dataType: 'json',
data: {
productId: productId,
fromUnitId: selectedUnitId,
quantity: quantity
},
success: function(resp){
if(resp && resp.success){
onDone(parseFloat(resp.result));
} else {
onDone(quantity);
}
},
error: function(){ onDone(quantity); }
});
}catch(e){ onDone(quantity); }
}
// Check if product already exists in sold list with same parameters
function issetInSoldList(productid, quantity, unitPrice, tax, discount, totalPurchasePrice) {
const foundProduct = soldList.find(product =>
product.productid === productid &&
product.quantity === quantity &&
product.unitPrice === unitPrice &&
product.tax === tax &&
product.discount === discount &&
product.totalPurchasePrice === totalPurchasePrice
);
return !!foundProduct;
}
function applyMasks() {
$('.mask-money').mask("##0.00", { reverse: true });
}
// Convert various input types to Decimal for precise calculations
function convertToDecimal(value) {
try {
if (value instanceof Decimal) {
return value;
}
if (value === undefined || value === null) {
return new Decimal(0);
}
if (typeof value === 'number') {
if (!isFinite(value) || isNaN(value)) {
return new Decimal(0);
}
return new Decimal(value);
}
if (typeof value === 'string') {
var sanitized = value.replace(/[^0-9.,-]/g, '').replace(/,/g, '').trim();
if (sanitized === '' || sanitized === '-' || sanitized === '.' || sanitized === '-.') {
return new Decimal(0);
}
return new Decimal(sanitized);
}
return new Decimal(0);
} catch (e) {
return new Decimal(0);
}
}
// Round value to specified decimal places (default 2)
function roundToDecimal(value, decimalPlaces = 2) {
const decimalValue = new Decimal(value);
const roundedValue = decimalValue.toDecimalPlaces(decimalPlaces, Decimal.ROUND_UP);
return roundedValue.toNumber();
}
// Round stock values down to prevent overselling
function roundStockToDecimal(value, decimalPlaces = 2) {
const decimalValue = new Decimal(value);
const roundedValue = decimalValue.toDecimalPlaces(decimalPlaces, Decimal.ROUND_DOWN);
return roundedValue.toNumber();
}
// Round to nearest cent for currency calculations
function roundToNearestCent(amount) {
return Math.round(amount * 100) / 100;
}
// Round to two decimal places
function roundToTwoDecimals(number) {
var factor = Math.pow(10, 2);
return (Math.round(number * factor) / factor).toFixed(2);
}
// Validate numeric input
function validate(s) {
var rgx = /^[0-9]*\.?[0-9]*$/;
return s.match(rgx);
}
// Convert string to float, handling commas
function convertToFloat(value) {
if (typeof value === "string") {
value = value.replace(',', '');
}
return parseFloat(value);
}
// Add thousand separators to numbers
function addCommas(number) {
let decimals = 2;
number = (number + '').replace(/[^0-9+\-Ee.]/g, '');
var n = !isFinite(+number) ? 0 : +number;
var prec = !isFinite(+decimals) ? 0 : Math.abs(decimals);
var sep = ',';
var dec = '.';
var s = '';
var toFixedFix = function(n, prec) {
var k = Math.pow(10, prec);
return '' + (Math.round(n * k) / k).toFixed(prec);
};
s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
if (s[0].length > 3) {
s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
}
if ((s[1] || '').length < prec) {
s[1] = s[1] || '';
s[1] += new Array(prec - s[1].length + 1).join('0');
}
return s.join(dec);
}
// Create popup window with specified dimensions
function createPopup(url, window_name = 'example-popup') {
var width = 1100;
var height = 700;
var screen_width = window.screen.width;
var screen_height = window.screen.height;
var popup_left = (screen_width - width) / 2;
var popup_top = (screen_height - height) / 2;
var popup_window = window.open(url, window_name, 'width=' + width + ',height=' + height + ',left=' + popup_left + ',top=' + popup_top);
}
// Show error alert using SweetAlert
function showErrorAlert(message) {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: '' + message,
background: 'white',
});
}
// Show success alert using SweetAlert
function showSuccessAlert(message) {
Swal.fire({
icon: 'success',
title: 'Başarılı',
text: '' + message,
background: 'white',
});
}
// =============================================================================
// PRODUCT MANAGEMENT FUNCTIONS
// =============================================================================
// Add product to sold list with validation
function addProductToSoldList(
productid,
productName,
code,
measurement,
quantity,
unitPrice,
tax,
discount,
totalPurchasePrice,
warehouseid,
unAllocatedQuantity,
thickness,
width,
height,
cost,
productType,
selectedUnitId = null
) {
var newQuantity = convertToDecimal(quantity).toNumber();
var newUnAllocatedQuantity = convertToDecimal(unAllocatedQuantity).toNumber();
var newPrice = convertToDecimal(unitPrice).toNumber();
var newTax = convertToDecimal(tax).toNumber();
var newDiscount = convertToDecimal(discount).toNumber();
var newTotalPurchasePrice = convertToDecimal(totalPurchasePrice).toNumber();
if (!issetInSoldList(productid, newQuantity, newPrice, newTax, newDiscount, newTotalPurchasePrice)) {
let productSold = {
uuid: crypto.randomUUID(),
productid: productid,
productName: productName,
code: code,
measurement: measurement,
quantity: newQuantity,
baseUnitQuantity: newQuantity,
unAllocatedQuantity: newUnAllocatedQuantity,
unitPrice: newPrice,
tax: newTax,
discount: newDiscount,
totalPurchasePrice: newTotalPurchasePrice,
warehouse: warehouseid,
thickness: thickness,
width: width,
height: height,
cost: cost,
productType: productType,
selectedUnitId: selectedUnitId,
}
soldList.push(productSold);
// compute base unit quantity asynchronously and update the item
computeBaseUnitQuantity(productid, selectedUnitId, newQuantity, function(baseQty){
const item = soldList.find(i => i.uuid === productSold.uuid);
if(item){ item.baseUnitQuantity = convertToDecimal(baseQty || newQuantity).toNumber(); }
});
fetchStocks();
fetchSoldListRows();
fetchPaymentForm();
$('#sales_form_totalPurchasePrice').val(addCommas(getTotalSoldAmount()));
return productSold;
} else {
console.log("List + ")
console.log(soldList)
throw new Error("Ürün zaten listede mevcut !")
}
}
// Remove product from sold list by UUID
function deleteProductInSoldList(uuid) {
soldList = soldList.filter(product =>
product.uuid !== uuid
);
fetchSoldListRows();
}
// Mark product as selected and unselect others
function selectProduct(productId) {
products.forEach(product => product.selected = false);
let product = products.find(product => product.productid === productId);
if (product) {
product.selected = true;
}
return products;
}
// Get currently selected product
function getSelectedProduct() {
return products.find(product => product.selected === true);
}
// Update product dropdown with available products
function updateProductSelect() {
$('#productselect').empty();
$('#productselect').append('<option value="0" data-image="image-not-found.webp">Ürün Seçin</option>');
$.each(products, function(index, value) {
var image = value.image;
var option = '<option value="' + value.id + '" data-image="' + image + '" class="stockoption">' + value.name + ' - ' + value.code + ' - ' + value.measurement + ' - ' + value.quantity + '</option>';
$('#productselect').append(option);
});
$('#productselect').attr('disabled', false);
}
// Load products from selected warehouse
function getProductsFromWarehouse() {
var id = $('#warehouseselect option:selected').val();
if (!id) {
return;
}
$.ajax({
url: "/warehouse-stocks/" + id,
dataType: 'JSON',
method: 'GET',
success: function(response) {
if (response.length > 0) {
if (soldList.length <= 0) {
products = [];
$.each(response, function(index, product) {
let productObj = {
code: product.code,
id: product.id,
image: product.image,
length: 0,
measurement: product.measurement,
name: product.name,
priceFob: 0,
priceNavlun: 0,
productid: product.productid,
purchaseTotalAmount: 0,
quantity: product.totalQuantity,
unAllocatedQuantity: 0,
thickness: 0,
totalQuantity: product.totalQuantity,
totalUnitPrice: 0,
width: 0,
cost: 0,
measurementUnit: product.measurementUnit,
measurementUnitId: product.measurementUnitId,
stock: 0,
warehouseid: $('#warehouseselect option:selected').val(),
convertibleUnits: [],
productType: product.productTypeEnum,
};
products.push(productObj);
});
updateProductSelect();
}
} else {
showErrorAlert('Seçmiş olduğunuz depoda hiç ürün stoğu bulunmamaktadır.');
}
},
error: function(error) {
showErrorAlert('Bilinmeyen bir sistem hatası oluştu lütfen tekrar deneyin.');
}
});
}
// Update stock quantities based on sold items
function fetchStocks() {
// const groupedSoldList = soldList.reduce((acc, soldProduct) => {
// if (!acc[soldProduct.productid]) {
// acc[soldProduct.productid] = 0;
// }
// acc[soldProduct.productid] += convertToDecimal(soldProduct.quantity).toNumber();
// return acc;
// }, {});
const groupedSoldList = soldList.reduce((acc, soldProduct) => {
const serviceTypes = ["INSTALLATION_SERVICE", "MAINTENANCE_SERVICE", "CONSULTATION_SERVICE", "TREE"];
const productTypeKey = soldProduct.productType ? (soldProduct.productType.key || soldProduct.productType.name) : null;
const isService = productTypeKey ? serviceTypes.includes(productTypeKey) : false;
if (isService) {
return acc;
}
if (!acc[soldProduct.productid]) {
acc[soldProduct.productid] = 0;
}
const quantityToDeduct = soldProduct.baseUnitQuantity || soldProduct.quantity;
acc[soldProduct.productid] += convertToDecimal(quantityToDeduct).toNumber();
return acc;
}, {});
products.forEach(product => {
if (groupedSoldList[product.productid]) {
const totalSoldQuantity = groupedSoldList[product.productid];
product.quantity = roundStockToDecimal(convertToDecimal(product.totalQuantity - totalSoldQuantity).toNumber());
} else {
product.quantity = roundStockToDecimal(convertToDecimal(product.totalQuantity).toNumber());
}
});
calculateAndWriteDiffBetweenPaymentAndSolds();
}
// Validate stock quantity before adding to sold list
// function checkStockQuantity(quantity) {
// let product = products.find(p => p.productid === parseInt(productId, 10));
// const productType = product.productType.key;
// if(productType === "INSTALLATION_SERVICE" || productType === "MAINTENANCE_SERVICE" || productType === "CONSULTATION_SERVICE" || productType === 'TREE'){
//
// $('#updateServiceModal').modal('show');
//
// }else{
// if (convertToFloat(quantity) > product.quantity) {
// throw new Error('Yetersiz stok. Ekeleyebileceğiniz maksimum stok: ' + product.quantity);
// resetAddProductForm();
// }
// }
//
// }
function checkStockQuantity() {
return new Promise((resolve, reject) => {
const selectedProduct = getSelectedProduct();
if (!selectedProduct) {
return reject(new Error('Lütfen bir ürün seçin.'));
}
const productType = selectedProduct.productType.key;
const isService = ["INSTALLATION_SERVICE", "MAINTENANCE_SERVICE", "CONSULTATION_SERVICE", "TREE"].includes(productType);
if (isService) {
// Eğer ürün bir hizmet ise, stok kontrolü yapma, işlemi onayla.
return resolve();
}
const quantityStr = $('#adder-allocated-quantity').val();
const fromUnitId = $('#adder-quantity-unit-select').val();
const requestedQuantity = convertToDecimal(quantityStr).toNumber();
// Eğer birim seçilmemişse veya miktar 0 ise, miktar kadarını stokla karşılaştır.
if (!fromUnitId || fromUnitId === '') {
if (requestedQuantity > selectedProduct.quantity) {
return reject(new Error(`Yetersiz stok. Eklenebilecek maksimum miktar: ${selectedProduct.quantity} ${selectedProduct.measurementUnit}`));
}
return resolve(requestedQuantity); // Temel birimdeki miktarı döndür
}
// Seçilen birimdeki miktarı, ürünün temel birimine çevir.
$.ajax({
url: '/admin/measurement/convert-to-product-unit',
method: 'POST',
dataType: 'json',
data: {
productId: selectedProduct.productid,
fromUnitId: fromUnitId,
quantity: requestedQuantity
},
success: function(resp) {
if (resp && resp.success) {
const requestedBaseQuantity = convertToDecimal(resp.result).toNumber();
// Temel birimdeki talep edilen miktar, stoktan büyük mü diye kontrol et.
if (requestedBaseQuantity > selectedProduct.quantity) {
reject(new Error(`Yetersiz stok. Talep edilen miktar (${quantityStr} ${$('#adder-quantity-unit-select option:selected').text()}), stoktaki ${selectedProduct.quantity} ${selectedProduct.measurementUnit}'den fazla.`));
} else {
resolve(requestedBaseQuantity); // Kontrol başarılı, temel birimdeki miktarı döndür.
}
} else {
reject(new Error('Birim dönüştürme sırasında bir hata oluştu. Stok kontrol edilemedi.'));
}
},
error: function() {
reject(new Error('Stok kontrolü sırasında sunucuya ulaşılamadı.'));
}
});
});
}
// Get product cost information from server
function getProductCost(pid) {
const url = '/product-cost/' + pid + '/' + getSelectedProduct().warehouseid;
$.ajax({
url: "/product-cost-detail/" + pid + '?warehouse=' + getSelectedProduct().warehouseid,
method: "POST",
dataType: "JSON",
success: function(data) {
var product = getSelectedProduct();
product.cost = data.cost;
product.measurementUnit = data.measurement;
$('.stock-info-span').html(data.stock.toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " " + data.measurement);
$('.cost-info-span').html(data.cost.toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " €");
$('.measurement-unit-info-span').html(data.measurement);
$('#hidden-cost-input').val(data.cost);
const detailsHtml = `
<div class="row align-items-center">
<div class="col-md-3">${product.name}</div>
<div class="col-md-3">
<strong>
<button type="button" class="btn btn-link btn-sm p-0 cost-popup-btn" data-url="${url}">Maliyet: ${data.cost.toFixed(2)} €</button>
</strong>
</div>
<div class="col-md-3"><strong>Stok:</strong> ${data.stock.toFixed(2)} ${data.measurement}</div>
<div class="col-md-3"><strong>Birim:</strong> ${data.measurement}</div>
</div>
`;
$('#info-details-content').html(detailsHtml);
return data;
},
error: function(data) {
$('#info-details-content').html('<div class="text-danger">Ürün maliyet bilgileri yüklenemedi.</div>');
}
});
}
// Generate HTML for a product row (Standard or AVOIR)
function generateProductRowHtml(value, options = {}) {
const isAvoir = options.isAvoir || false;
const readonly = isAvoir ? 'readonly disabled' : '';
const inputReadonly = isAvoir ? 'readonly' : ''; // For inputs we might want just readonly, not disabled
const rowClass = isAvoir ? 'bg-light text-danger' : '';
const uuid = value.uuid;
// Render Service Button (only for standard rows)
const serviceBtnHtml = !isAvoir ? renderServiceEditBtn(value) : '';
// Actions
let actionButtonsHtml = '';
if (!isAvoir) {
actionButtonsHtml = `
<div class="btn-group" role="group">
${serviceBtnHtml}
<button type="button" class="btn btn-outline-secondary btn-sm open-unallocated-modal-row"
data-uuid="${uuid}" title="{% trans %}unAllocatedQuantity{% endtrans %}">
<i class="fas fa-box-open"></i>
<span class="badge badge-light ml-1 unalloc-display">${value.unAllocatedQuantity.toFixed(2)}</span>
</button>
<button type="button" class="btn btn-danger btn-sm delete-row-button"
data-uuid="${uuid}" title="Sil">
<i class="fas fa-trash"></i>
</button>
</div>
`;
} else {
// Empty for AVOIR
actionButtonsHtml = '';
}
// Info Button
let infoButtonHtml = '';
if (!isAvoir) {
infoButtonHtml = `
<button type="button" class="btn btn-sm btn-info btn-icon show-row-info-btn"
data-uuid="${uuid}" title="Ürün Bilgileri">
<i class="fas fa-info-circle"></i>
</button>
`;
}
// Product Select / Name
let productSelectHtml = '';
if (isAvoir) {
// For AVOIR, we just show a static input or disabled select mimicking the look
productSelectHtml = `
<input type="text" class="form-control text-danger font-weight-bold" value="AVOIR" readonly>
`;
} else {
let productOptions = '';
const serviceTypes = ['INSTALLATION_SERVICE','MAINTENANCE_SERVICE','CONSULTATION_SERVICE','TREE'];
const isServiceRow = (function(pt){
try {
if (!pt) return false;
if (typeof pt === 'string') return serviceTypes.includes(pt);
const key = pt.key || pt.name;
return serviceTypes.includes(key);
} catch(e) { return false; }
})(value.productType);
products.forEach(function(product) {
const isSelected = product.productid === value.productid ? 'selected' : '';
const displayName = (isSelected && isServiceRow && value.productName) ? value.productName : product.name;
productOptions += `<option value="${product.productid}" ${isSelected}>${displayName} - ${product.measurement} - ${product.quantity} - ${product.code}</option>`;
});
productSelectHtml = `
<select name="productId[]" class="form-control product-select-row" data-uuid="${uuid}">
${productOptions}
</select>
`;
}
// Unallocated Hidden Inputs
const unAllocatedHidens = !isAvoir ? `
<input type="hidden" name="warehouseid[]" value="${value.warehouse}">
<input type="hidden" name="length[]" value="${value.length || ''}">
<input type="hidden" name="width[]" value="${value.width || ''}">
<input type="hidden" name="thickness[]" value="${value.thickness || ''}">
<input type="hidden" name="cost[]" value="${value.cost || ''}">
<input type="hidden" name="unAllocatedQuantity[]" class="unallocated-quantity-hidden" value="${value.unAllocatedQuantity}" data-uuid="${uuid}">
<input type="hidden" name="unAllocatedQuantityUnit[]" class="unallocated-unit-hidden" value="" data-uuid="${uuid}">
` : '';
// Quantity Unit Select
let quantityUnitSelectHtml = '';
if(isAvoir){
quantityUnitSelectHtml = `
<select class="custom-select custom-select-sm" disabled>
<option>-</option>
</select>
`;
} else {
quantityUnitSelectHtml = `
<select class="custom-select custom-select-sm quantity-unit-select"
name="quantityUnit[]" data-uuid="${uuid}">
<option value="">-</option>
</select>
`;
}
// Discount Options
let discountOptionsHtml = '';
const discounts = [0, 1, 2, 3, 4, 5, 20, 30, 100];
discounts.forEach(function(d){
discountOptionsHtml += `<option value="${d}" ${value.discount === d ? 'selected' : ''}>% ${d}</option>`;
});
// Tax Options
let taxOptionsHtml = '';
const taxes = [0, 10, 20];
taxes.forEach(function(t){
const disabledAttr = t === 0 ? 'disabled' : '';
taxOptionsHtml += `<option value="${t}" ${value.tax === t ? 'selected' : ''} ${disabledAttr}>% ${t}</option>`;
});
const quantityVal = value.quantity !== undefined ? value.quantity.toFixed(2) : '1.00';
const unitPriceVal = value.unitPrice !== undefined ? value.unitPrice.toFixed(2) : '0.00';
const totalPriceVal = value.totalPurchasePrice !== undefined ? value.totalPurchasePrice.toFixed(2) : '0.00';
// Only standard rows get the name="..." attributes to be submitted
const nameAttr = isAvoir ? '' : 'name=';
return `
<tr id="${uuid}" data-product-id="${value.productid || ''}" class="${rowClass}">
<td>${infoButtonHtml}</td>
<td class="product-select-cell">
${productSelectHtml}
${unAllocatedHidens}
</td>
<td>
<div class="input-group input-group-sm" style="max-width: 220px;">
<input type="number" ${!isAvoir ? 'name="quantity[]"' : ''}
class="form-control quantity-input-row mask-money ${isAvoir ? 'text-danger' : ''}"
value="${quantityVal}"
min="0" step="0.01" data-uuid="${uuid}" ${inputReadonly}>
<div class="input-group-append">
${quantityUnitSelectHtml}
</div>
</div>
</td>
<td>
<input type="text" ${!isAvoir ? 'name="unitPrice[]"' : ''}
class="form-control form-control-sm price-input-row mask-money ${isAvoir ? 'text-danger' : ''}"
value="${unitPriceVal}"
min="0" step="0.01" data-uuid="${uuid}"
placeholder="Birim Fiyat" ${inputReadonly}>
</td>
<td>
<select ${!isAvoir ? 'name="discount[]"' : ''} class="form-control form-control-sm discount-select-row"
data-uuid="${uuid}" ${readonly}>
${discountOptionsHtml}
</select>
</td>
<td>
<select ${!isAvoir ? 'name="tax[]"' : ''} class="form-control form-control-sm tax-select-row"
data-uuid="${uuid}" ${readonly}>
${taxOptionsHtml}
</select>
</td>
<td class="text-right">
<input ${!isAvoir ? 'name="totalPrice[]"' : ''} type="text"
class="form-control form-control-sm total-input-row mask-money ${isAvoir ? 'text-danger font-weight-bold' : ''}"
value="${totalPriceVal}"
data-uuid="${uuid}"
placeholder="Toplam" ${inputReadonly}/>
</td>
<td class="text-center">
${actionButtonsHtml}
</td>
</tr>
${!isAvoir ? `
<tr class="info-details-row-class" data-uuid="${uuid}" style="display: none;">
<td colspan="8" style="padding: 0; border-top: none;">
<div class="info-details-content-class p-3" style="display: none;">
<div class="row">
<div class="col-md-3"><strong>Ürün Adı:</strong> ${value.productName}</div>
<div class="col-md-3"><strong>Kod:</strong> ${value.code}</div>
<div class="col-md-3"><strong>Ölçü:</strong> ${value.measurement}</div>
<div class="col-md-3"><strong>Uzunluk:</strong> ${value.length || '-'}</div>
</div>
<div class="row mt-2">
<div class="col-md-3"><strong>Genişlik:</strong> ${value.width || '-'}</div>
<div class="col-md-3"><strong>Kalınlık:</strong> ${value.thickness || '-'}</div>
<div class="col-md-3"><strong>Maliyet:</strong> ${(value.cost || 0).toFixed(2)} €</div>
<div class="col-md-3"><strong>Depo:</strong> ${value.warehouse}</div>
</div>
</div>
</td>
</tr>` : ''}
`;
}
// Render sold products table rows with complete editing capabilities
// TODO: Seçilen ürün eğer hizmet ise dropdown da hizmet adını kaydedildiği şekilde göster
function fetchSoldListRows() {
$('#product-list-table-tbody').empty();
soldList.forEach(function(value, index) {
const rowHtml = generateProductRowHtml(value, { isAvoir: false });
$('#product-list-table-tbody').append(rowHtml);
$(`.product-select-row[data-uuid="${value.uuid}"]`).select2({
theme: 'bootstrap4',
dropdownCssClass: 'custom-select-dropdown',
placeholder: 'Ürün Seçin',
allowClear: true
});
applyMasks();
// Load measurement units for each product row with default unit
const product = products.find(p => p.productid === value.productid);
const defaultUnitId = product ? product.measurementUnitId : null;
const preferredUnitId = (value.selectedUnitId !== undefined && value.selectedUnitId !== null && value.selectedUnitId !== '') ? value.selectedUnitId : defaultUnitId;
loadMeasurementUnitsForRow(value.productid, value.uuid, preferredUnitId);
});
// Check for Gift Voucher amount and add AVOIR row using reusable component
const totalVoucherAmount = calculateTotalGiftVoucherAmount();
if (!totalVoucherAmount.isZero()) {
const avoirItem = {
uuid: 'avoir-row',
productid: null,
productName: 'AVOIR',
quantity: 1,
unAllocatedQuantity: 0,
unitPrice: totalVoucherAmount.times(-1).toNumber(), // Negative unit price
totalPurchasePrice: totalVoucherAmount.times(-1).toNumber(), // Negative total
discount: 0,
tax: 0
};
// We override the visual display of the price to be positive in the input if desired,
// OR we keep it negative to indicate deduction.
// The user request screenshot showed "AVOIR" in red.
// Standard AVOIR usually implies negative context but let's stick to the styling.
// IMPORTANT: The screenshot usually shows the total as negative, so unit price should likely be negative too.
// Wait, if Quantity is 1 and Total is -Amount, Unit Price must be -Amount.
const avoirRowHtml = generateProductRowHtml(avoirItem, { isAvoir: true });
$('#product-list-table-tbody').append(avoirRowHtml);
}
fetchStocks();
updateProductSelect();
updateTotalAmounts();
$('#warehouseselect').attr('disabled', soldList.length > 0);
}
// --- GIFT VOUCHER INTEGRATION ---
// Set default ID to 0 or a value that won't match if not found.
// Using Twig to find the ID.
const PAYMENT_METHOD_GIFT_VOUCHER_ID = (function() {
{% for pm in paymentMethods %}
{% if pm.name == 'Hediye Çeki' or pm.name == 'Gift Voucher' %}
return "{{ pm.id }}";
{% endif %}
{% endfor %}
return null;
})();
function calculateTotalGiftVoucherAmount() {
let totalVoucher = new Decimal(0);
if (!PAYMENT_METHOD_GIFT_VOUCHER_ID) return totalVoucher;
payments.forEach(function(p) {
if (String(p.paymentMethod) === String(PAYMENT_METHOD_GIFT_VOUCHER_ID)) {
totalVoucher = totalVoucher.plus(convertToDecimal(p.amount));
}
});
return totalVoucher;
}
// ---------------------------------
function renderServiceEditBtn(value){
const product = products.find(p => p.productid === value.productid);
if (isProductService(product.productid)) {
return `
<button type="button"
class="btn btn-warning btn-sm edit-service-button"
data-uuid="${value.uuid}"
title="Servisi Güncelle">
<i class="fas fa-edit"></i>
</button>
`;
}
return ''; // değilse hiç ekleme
}
function calculateRowFinancials(source) {
const quantity = convertToDecimal(source.quantity || 0);
const unAllocatedQuantity = convertToDecimal(source.unAllocatedQuantity || 0);
const unitPrice = convertToDecimal(source.unitPrice || 0);
const discountPercentage = convertToDecimal(source.discount || 0);
const taxPercentage = convertToDecimal(source.tax || 0);
const totalQuantity = quantity.plus(unAllocatedQuantity);
if (totalQuantity.isZero()) {
return {
subtotal: new Decimal(0),
discountAmount: new Decimal(0),
afterDiscount: new Decimal(0),
taxAmount: new Decimal(0),
total: new Decimal(0),
totalRounded: new Decimal(0),
taxPercentage
};
}
const subtotal = totalQuantity.mul(unitPrice);
const discountAmount = subtotal.mul(discountPercentage.div(100));
const afterDiscount = subtotal.minus(discountAmount);
const taxAmount = afterDiscount.mul(taxPercentage.div(100));
const total = afterDiscount.plus(taxAmount);
return {
subtotal,
discountAmount,
afterDiscount,
taxAmount,
total,
totalRounded: total.toDecimalPlaces(2, MONEY_ROUNDING_MODE),
taxPercentage
};
}
function aggregateSoldTotals() {
const aggregation = {
grossTotal: new Decimal(0),
totalDiscount: new Decimal(0),
subtotal: new Decimal(0),
tax: new Decimal(0),
grandTotal: new Decimal(0),
byTaxRate: {}
};
soldList.forEach(function(item) {
// item.totalPurchasePrice'ı kullan (manuel veya otomatik hesaplanmış)
const itemTotal = convertToDecimal(item.totalPurchasePrice || 0);
// Toplam üzerinden geriye doğru hesaplama
// Formül: total = subtotal × (1 - discount%) × (1 + tax%)
// Ters formül: afterDiscount = total / (1 + tax%)
// subtotal = afterDiscount / (1 - discount%)
const taxPercentage = convertToDecimal(item.tax || 0);
const discountPercentage = convertToDecimal(item.discount || 0);
const taxMultiplier = new Decimal(1).plus(taxPercentage.div(100));
// Adım 1: KDV'yi çıkar → İndirim sonrası tutarı bul
// afterDiscount = total / (1 + tax%)
const afterDiscount = itemTotal.div(taxMultiplier);
// Adım 2: KDV tutarını hesapla
// taxAmount = afterDiscount × tax%
const taxAmount = afterDiscount.mul(taxPercentage.div(100));
// NOT: "Ara Toplam" (subtotal) = afterDiscount (indirim uygulanmış tutar)
// Çünkü görüntülenen "Ara Toplam" indirim sonrası, KDV öncesi tutar
// Calculate Gross (before discount)
const quantity = convertToDecimal(item.quantity).plus(convertToDecimal(item.unAllocatedQuantity));
const unitPrice = convertToDecimal(item.unitPrice);
const grossLine = quantity.mul(unitPrice);
// Calculate Discount derived from Gross vs Net
const discountAmount = grossLine.minus(afterDiscount);
aggregation.grossTotal = aggregation.grossTotal.plus(grossLine);
aggregation.totalDiscount = aggregation.totalDiscount.plus(discountAmount);
aggregation.subtotal = aggregation.subtotal.plus(afterDiscount);
aggregation.tax = aggregation.tax.plus(taxAmount);
aggregation.grandTotal = aggregation.grandTotal.plus(itemTotal);
const rateKey = taxPercentage.toFixed(4);
if (!aggregation.byTaxRate[rateKey]) {
aggregation.byTaxRate[rateKey] = {
rate: taxPercentage,
amount: new Decimal(0)
};
}
aggregation.byTaxRate[rateKey].amount = aggregation.byTaxRate[rateKey].amount.plus(taxAmount);
});
aggregation.subtotalRounded = aggregation.subtotal.toDecimalPlaces(2, MONEY_ROUNDING_MODE);
aggregation.taxRounded = aggregation.tax.toDecimalPlaces(2, MONEY_ROUNDING_MODE);
aggregation.grandTotalRounded = aggregation.grandTotal.toDecimalPlaces(2, MONEY_ROUNDING_MODE);
return aggregation;
}
// Track which row is being updated to prevent circular updates
let rowUpdatingFromTotal = {};
let rowUpdatingFromUnitPrice = {};
// Track rows where total was manually entered (should not recalculate total)
let rowHasManualTotal = {};
function updateSoldProductFromRow(uuid) {
const itemIndex = soldList.findIndex(item => item.uuid === uuid);
if (itemIndex === -1) {
return;
}
// Eğer bu satır toplam tarafından güncelleniyorsa, tekrar toplam güncelleme
if(rowUpdatingFromTotal[uuid]) {
return;
}
rowUpdatingFromUnitPrice[uuid] = true;
const $row = $(`#${uuid}`);
const quantity = convertToDecimal($row.find('.quantity-input-row').val()).toNumber();
const unitPrice = convertToDecimal($row.find('.price-input-row').val()).toNumber();
const discount = convertToDecimal($row.find('.discount-select-row').val()).toNumber();
const tax = convertToDecimal($row.find('.tax-select-row').val()).toNumber();
soldList[itemIndex].quantity = quantity;
soldList[itemIndex].unitPrice = unitPrice;
soldList[itemIndex].discount = discount;
soldList[itemIndex].tax = tax;
const selectedUnitId = $row.find('.quantity-unit-select').val();
computeBaseUnitQuantity(soldList[itemIndex].productid, selectedUnitId, quantity, function(baseQty){
soldList[itemIndex].baseUnitQuantity = convertToDecimal(baseQty || quantity).toNumber();
});
// Eğer bu satırda manuel toplam girildiyse, toplam sabit kalmalı, sadece birim fiyat güncellensin
if (rowHasManualTotal[uuid]) {
// Manuel toplam korunuyor, birim fiyatı tersine hesapla
const manualTotal = convertToDecimal(soldList[itemIndex].totalPurchasePrice);
const totalQuantity = convertToDecimal(quantity).plus(convertToDecimal(soldList[itemIndex].unAllocatedQuantity || 0));
if (!totalQuantity.isZero()) {
const discountMultiplier = new Decimal(1).minus(convertToDecimal(discount).div(100));
const taxMultiplier = new Decimal(1).plus(convertToDecimal(tax).div(100));
if (discountMultiplier.isZero()) {
soldList[itemIndex].unitPrice = 0;
$row.find('.price-input-row').val('0.00');
} else if (!taxMultiplier.isZero()) {
const calculatedUnitPrice = manualTotal
.div(taxMultiplier)
.div(discountMultiplier)
.div(totalQuantity)
.toDecimalPlaces(6, MONEY_ROUNDING_MODE);
soldList[itemIndex].unitPrice = calculatedUnitPrice.toNumber();
$row.find('.price-input-row').val(calculatedUnitPrice.toDecimalPlaces(2, MONEY_ROUNDING_MODE).toFixed(2));
}
}
// Toplam input'u DEĞİŞTİRME - manuel değer korunuyor
} else {
// Normal hesaplama - toplam otomatik hesaplansın
const rowTotals = calculateRowFinancials(soldList[itemIndex]);
soldList[itemIndex].totalPurchasePrice = rowTotals.totalRounded.toNumber();
$row.find('.total-input-row').val(rowTotals.totalRounded.toFixed(2));
}
updateTotalAmounts();
fetchStocks();
fetchPaymentForm();
setTimeout(function(){ delete rowUpdatingFromUnitPrice[uuid]; }, 0);
}
// Toplam tutar değiştirildiğinde birim fiyatı tersine hesaplar
function updateUnitPriceFromTotal(uuid) {
const itemIndex = soldList.findIndex(item => item.uuid === uuid);
if (itemIndex === -1) {
return;
}
// Eğer bu satır birim fiyat tarafından güncelleniyorsa, döngüyü engelle
if(rowUpdatingFromUnitPrice[uuid]) {
return;
}
rowUpdatingFromTotal[uuid] = true;
// ÖNEMLİ: Toplam manuel girildi, bu satırı işaretle
rowHasManualTotal[uuid] = true;
const $row = $(`#${uuid}`);
const totalValue = convertToDecimal($row.find('.total-input-row').val());
const item = soldList[itemIndex];
const totalQuantity = convertToDecimal(item.quantity || 0).plus(convertToDecimal(item.unAllocatedQuantity || 0));
if (totalQuantity.isZero()) {
setTimeout(function(){ delete rowUpdatingFromTotal[uuid]; }, 0);
return;
}
// Manuel girilen toplam değerini kaydet
soldList[itemIndex].totalPurchasePrice = totalValue.toNumber();
// Eğer toplam 0 ise, birim fiyat da 0 olmalı
if (totalValue.isZero()) {
$row.find('.price-input-row').val('0.00');
soldList[itemIndex].unitPrice = 0;
updateTotalAmounts();
fetchStocks();
fetchPaymentForm();
setTimeout(function(){ delete rowUpdatingFromTotal[uuid]; }, 0);
return;
}
const discountMultiplier = new Decimal(1).minus(convertToDecimal(item.discount || 0).div(100));
const taxMultiplier = new Decimal(1).plus(convertToDecimal(item.tax || 0).div(100));
// %100 indirimde discountMultiplier = 0 olur, bu durumda birim fiyat 0 olmalı
if (discountMultiplier.isZero()) {
$row.find('.price-input-row').val('0.00');
soldList[itemIndex].unitPrice = 0;
updateTotalAmounts();
fetchStocks();
fetchPaymentForm();
setTimeout(function(){ delete rowUpdatingFromTotal[uuid]; }, 0);
return;
}
if (taxMultiplier.isZero()) {
setTimeout(function(){ delete rowUpdatingFromTotal[uuid]; }, 0);
return;
}
// Formül: unitPrice = total / ((1 + tax%) * (1 - discount%) * quantity)
const unitPrice = totalValue
.div(taxMultiplier)
.div(discountMultiplier)
.div(totalQuantity)
.toDecimalPlaces(6, MONEY_ROUNDING_MODE);
$row.find('.price-input-row').val(unitPrice.toDecimalPlaces(2, MONEY_ROUNDING_MODE).toFixed(2));
soldList[itemIndex].unitPrice = unitPrice.toNumber();
// Sadece stok ve genel toplamları güncelle, satır toplamını değiştirme
updateTotalAmounts();
fetchStocks();
fetchPaymentForm();
setTimeout(function(){ delete rowUpdatingFromTotal[uuid]; }, 0);
}
// =============================================================================
// WAREHOUSE SELECTION EVENT HANDLER
// =============================================================================
// Handle warehouse selection change
$('#warehouseselect').on('change', function() {
getProductsFromWarehouse();
});
// Auto-select warehouse if there is only one
var $warehouseSelect = $('#warehouseselect');
if ($warehouseSelect.find('option').length === 2 && $warehouseSelect.val() === "") {
var firstWarehouseVal = $warehouseSelect.find('option').eq(1).val();
$warehouseSelect.val(firstWarehouseVal).trigger('change');
}
// Load measurement units for a specific product row (with caching)
let measurementUnitsCache = {};
function loadMeasurementUnitsForRow(productId, uuid, defaultUnitId = null) {
// Check cache first
if (measurementUnitsCache[productId]) {
populateUnitSelectForRow(uuid, measurementUnitsCache[productId], defaultUnitId);
return;
}
// Load from server if not cached
$.get('/product/' + productId + '/measurement-units', function(units) {
// Cache the units
measurementUnitsCache[productId] = units || [];
populateUnitSelectForRow(uuid, units, defaultUnitId);
}).fail(function() {
console.log('Failed to load measurement units for product:', productId);
measurementUnitsCache[productId] = []; // Cache empty array to prevent repeated requests
});
}
// Populate unit select dropdown for a specific row
function populateUnitSelectForRow(uuid, units, defaultUnitId = null) {
const $select = $(`.quantity-unit-select[data-uuid="${uuid}"]`);
$select.empty();
if (Array.isArray(units) && units.length > 0) {
units.forEach(function(unit) {
const isSelected = defaultUnitId !== null && defaultUnitId !== undefined && defaultUnitId !== '' && String(unit.id) === String(defaultUnitId);
const selectedAttr = isSelected ? ' selected' : '';
$select.append('<option value="' + unit.id + '"' + selectedAttr + '>' + unit.symbol + '</option>');
});
if (units.length === 1) {
$select.prop('disabled', true);
} else {
$select.prop('disabled', false);
}
} else {
$select.append('<option value="">-</option>');
$select.prop('disabled', false);
}
}
// Update total amounts in footer
function updateTotalAmounts() {
const taxLabelPrefix = "TVA";
const taxTotalLabel = "{{ 'total'|trans|e('js') }}";
const formatTaxRate = function(rateDecimal) {
const rateNumber = convertToDecimal(rateDecimal || 0).toNumber();
if (!isFinite(rateNumber)) {
return "0";
}
if (Math.abs(rateNumber % 1) < 1e-6) {
return rateNumber.toFixed(0);
}
return rateNumber.toFixed(2).replace(/\.?0+$/, "");
};
const totals = aggregateSoldTotals();
const $tfoot = $("#product-list-table tfoot");
$tfoot.empty();
// Helper to create row
const addRow = (label, value, isBold=false, colorClass='') => {
const row = $(`<tr class="summary-row ${isBold ? 'font-weight-bold' : ''} ${colorClass}">
<td colspan="8">
<div class="sale-summary-row">
<span class="summary-label">${label}</span>
<span class="summary-value">${formatCurrency(value)}</span>
</div>
</td>
</tr>`);
$tfoot.append(row);
};
// 1. Total H.T. (Gross)
addRow('Total H.T.:', totals.grossTotal, true);
// 2. Remise
if (!totals.totalDiscount.isZero() && totals.totalDiscount.toNumber() > 0.005) {
const $row = $(`<tr class="summary-row">
<td colspan="8">
<div class="sale-summary-row">
<span class="summary-label">Remise:</span>
<span class="summary-value">${formatCurrency(totals.totalDiscount.negated())}</span>
</div>
</td>
</tr>`);
$tfoot.append($row);
}
// 3. Total H.T. (Net)
addRow('Total H.T.:', totals.subtotal, true);
// 4. AVOIR
const totalVoucherAmount = calculateTotalGiftVoucherAmount();
let finalTotal = totals.grandTotalRounded; // Assuming totals.grandTotalRounded is available in result? Yes
// Wait, aggregateSoldTotals returns grandTotalRounded.
// But calculateSaleSummary in edit.html.twig returned summary.roundedTotal.
if (!totalVoucherAmount.isZero()) {
const $avoirRow = $(`<tr class="summary-row text-danger font-weight-bold">
<td colspan="8">
<div class="sale-summary-row">
<span class="summary-label">AVOIR:</span>
<span class="summary-value">-${formatCurrency(totalVoucherAmount)}</span>
</div>
</td>
</tr>`);
$tfoot.append($avoirRow);
finalTotal = finalTotal.minus(totalVoucherAmount);
}
// 5. TVA
const taxEntries = Object.values(totals.byTaxRate)
.filter(function(entry) { return !entry.amount.isZero(); })
.sort(function(a, b) { return a.rate.toNumber() - b.rate.toNumber(); });
if (taxEntries.length) {
taxEntries.forEach(function(entry) {
addRow(taxLabelPrefix + " (" + formatTaxRate(entry.rate) + "%):", entry.amount);
});
} else {
addRow(taxLabelPrefix + " (0%):", 0);
}
// 6. TOTAL
const $totalRow = $(`<tr class="total-row summary-row font-weight-bold">
<td colspan="8">
<div class="sale-summary-row summary-total">
<span class="summary-label">TOTAL À PAYER (EUR):</span>
<span class="summary-value display-5">${formatCurrency(finalTotal)}</span>
</div>
</td>
</tr>`);
$tfoot.append($totalRow);
calculateAndWriteDiffBetweenPaymentAndSolds();
}
// Update sold list item by UUID
function updateSoldListItem(uuid, field, value) {
const itemIndex = soldList.findIndex(item => item.uuid === uuid);
if (itemIndex === -1) {
return;
}
soldList[itemIndex][field] = convertToDecimal(value).toNumber();
updateSoldProductFromRow(uuid);
}
// Satıştaki miktarları listeden düşme Un Allocated HARİÇ tutulur
function fetchStocks() {
// soldList'i productid'e göre grupla
const groupedSoldList = soldList.reduce((acc, soldProduct) => {
if (!acc[soldProduct.productid]) {
acc[soldProduct.productid] = 0;
}
acc[soldProduct.productid] += convertToDecimal(soldProduct.baseUnitQuantity).toNumber();
return acc;
}, {});
// products listesindeki totalQuantity'yi güncelle
products.forEach(product => {
if (groupedSoldList[product.productid]) {
const totalSoldQuantity = groupedSoldList[product.productid];
product.quantity = roundStockToDecimal(convertToDecimal(product.totalQuantity - totalSoldQuantity).toNumber());
} else {
// Eğer soldList'te bu ürün yoksa, quantity aynı kalır
product.quantity = roundStockToDecimal(convertToDecimal(product.totalQuantity).toNumber());
}
});
calculateAndWriteDiffBetweenPaymentAndSolds();
}
function getTotalSoldAmount() {
return aggregateSoldTotals().grandTotalRounded.toNumber();
}
$(document).on('blur', '#payment-list-table-tbody .payment-amount-input', function() {
// Buraya kod eklenebilir
});
// Gelen değeri decimale çevirme
function convertToDecimal(value) {
try {
if (value instanceof Decimal) {
return value;
}
if (value === undefined || value === null) {
return new Decimal(0);
}
if (typeof value === 'number') {
if (!isFinite(value) || isNaN(value)) {
return new Decimal(0);
}
return new Decimal(value);
}
if (typeof value === 'string') {
// Keep digits, minus, dot and comma; strip others
var sanitized = value.replace(/[^0-9.,-]/g, '').replace(/,/g, '').trim();
if (sanitized === '' || sanitized === '-' || sanitized === '.' || sanitized === '-.') {
return new Decimal(0);
}
return new Decimal(sanitized);
}
return new Decimal(0);
} catch (e) {
return new Decimal(0);
}
}
// Seçilen ürünün birimini dropdown'da gösterme (handled by main product select handler)
function clearUnitSelects(){
$('#adder-quantity-unit-select').empty().append('<option value="">-</option>');
$('#adder-unallocated-quantity-unit-select').empty().append('<option value="">-</option>');
$('#modal-unallocated-unit').empty().append('<option value="">-</option>');
}
// Select içine çevrilebilir birimleri koyuyor
function populateSelectWithUnits($select, units) {
$select.empty();
if (Array.isArray(units) && units.length > 0) {
units.forEach(function(unit) {
$select.append('<option value="' + unit.id + '">' + unit.symbol + '</option>');
});
if (units.length === 1) {
$select.prop('disabled', true);
} else {
$select.prop('disabled', false);
}
} else {
$select.append('<option value="">-</option>');
$select.prop('disabled', false);
}
}
// Ürünün çevrilebilir birimlerini select içine koyuyoruz.
function refreshUnitSelectsForProduct(product) {
if (!product || !product.convertibleUnits) {
clearUnitSelects();
return;
}
populateSelectWithUnits($('#adder-quantity-unit-select'), product.convertibleUnits);
populateSelectWithUnits($('#adder-unallocated-quantity-unit-select'), product.convertibleUnits);
populateSelectWithUnits($('#modal-unallocated-unit'), product.convertibleUnits);
}
// TODO: Birimler doldurulurken ilgili birimin karşılığındaki miktar yazılacak
function fillUnitSelects(product){
// Check cache first
if (measurementUnitsCache[product.productid]) {
product.convertibleUnits = measurementUnitsCache[product.productid];
refreshUnitSelectsForProduct(product);
computeAndShowBaseUnitEquivalent();
return;
}
$.get('/product/' + product.productid + '/measurement-units', function(units) {
// Cache the units
measurementUnitsCache[product.productid] = units || [];
// Store in product object
product.convertibleUnits = units || [];
// Update selects
refreshUnitSelectsForProduct(product);
// Set default unit as selected
if (product.measurementUnitId) {
$('#adder-quantity-unit-select').val(product.measurementUnitId);
$('#adder-unallocated-quantity-unit-select').val(product.measurementUnitId);
$('#modal-unallocated-unit').val(product.measurementUnitId);
}
computeAndShowBaseUnitEquivalent();
});
}
// Seçilen birimde girilen miktarın, ürünün KENDİ birimindeki karşılığını hesaplar ve inputun altına küçük yazıyla gösterir
function computeAndShowBaseUnitEquivalent() {
try {
const selectedProduct = getSelectedProduct();
if (!selectedProduct || !selectedProduct.productid) {
updateAllocatedQuantityHint('');
return;
}
const fromUnitId = $('#adder-quantity-unit-select').val();
const qtyStr = $('#adder-allocated-quantity').val();
// If base unit is selected, do not show duplicate label
if (fromUnitId && String(fromUnitId) === String(selectedProduct.measurementUnitId)) {
updateAllocatedQuantityHint('');
return;
}
if (!fromUnitId || fromUnitId === '' || qtyStr === '' || qtyStr === null) {
updateAllocatedQuantityHint('');
return;
}
const quantity = convertToDecimal(qtyStr).toNumber();
if (isNaN(quantity)) {
updateAllocatedQuantityHint('');
return;
}
$.ajax({
url: '/admin/measurement/convert-to-product-unit',
method: 'POST',
dataType: 'json',
data: {
productId: selectedProduct.productid,
fromUnitId: fromUnitId,
quantity: quantity
},
success: function(resp) {
if (resp && resp.success) {
const unitText = selectedProduct.measurementUnit ? (' ' + selectedProduct.measurementUnit) : '';
updateAllocatedQuantityHint((Number(resp.result).toFixed(2)) + unitText);
} else {
updateAllocatedQuantityHint('');
}
},
error: function() {
updateAllocatedQuantityHint('');
}
});
} catch (e) {
updateAllocatedQuantityHint('');
}
}
// Base unit tipinden miktar.
function updateAllocatedQuantityHint(text) {
const $group = $('#adder-allocated-quantity').closest('.input-group');
if ($('#allocated-quantity-converted-hint').length === 0) {
$('<small id="allocated-quantity-converted-hint" class="form-text text-muted"></small>').insertAfter('#adder-quantity-unit-select');
}
$('#allocated-quantity-converted-hint').text(text || '');
}
function roundToDecimal(value, decimalPlaces = 2) {
const decimalValue = new Decimal(value);
const roundedValue = decimalValue.toDecimalPlaces(decimalPlaces, Decimal.ROUND_UP);
return roundedValue.toNumber();
}
function roundStockToDecimal(value, decimalPlaces = 2) {
const decimalValue = new Decimal(value);
const roundedValue = decimalValue.toDecimalPlaces(decimalPlaces, Decimal.ROUND_DOWN);
return roundedValue.toNumber();
}
function selectProduct(productId) {
products.forEach(product => product.selected = false);
let product = products.find(product => product.productid === productId);
if (product) {
product.selected = true;
}
return products;
}
function getSelectedProduct() {
return products.find(product => product.selected === true);
}
// Ürün seçme selectini yeniden güncelleme.
function updateProductSelect() {
$('#productselect').empty();
$('#productselect').append('<option value="0" data-image="image-not-found.webp">Ürün Seçin</option>');
$.each(products, function(index, value) {
var image = value.image;
var option = '<option value="' + value.id + '" data-image="' + image + '" class="stockoption">' + value.name + ' - ' + value.code + ' - ' + value.measurement + ' - ' + value.quantity + '</option>';
$('#productselect').append(option);
});
$('#productselect').attr('disabled', false);
}
$('#products_sold_quantity').mask("##0.00", {
reverse: true
});
$('#adder-allocated-quantity').mask("##0.00", {
reverse: true
});
$('#adder-unallocated-quantity').mask("##0.00", {
reverse: true
});
$('#adder-price').mask("##0.00", {
reverse: true
});
$('#adder-total').mask("##0.00", {
reverse: true
});
$('#products_sold_unitPriceFob').mask("#,##0.00", {
reverse: true
});
$('#products_sold_unitPriceNavlun').mask("#,##0.00", {
reverse: true
});
$('#products_sold_totalUnitPrice').mask("#,##0.00", {
reverse: true
});
$('#products_sold_totalUnitPurchasePrice').mask("#,##0.00", {
reverse: true
});
$('#products_sold_totalPrice').mask("#,##0.00", {
reverse: true
});
$('#products_sold_totalPuchasePrice').mask("#,##0.00", {
reverse: true
});
$('#sales_form_totalPurchasePrice').mask("#,##0.00", {
reverse: true
});
$('#sales_form_totalPurchasePrice').attr('readonly', 'readonly');
var totalQuantity = 0;
var productId = 0;
var productname = $('#products_sold_productName');
var productmeasurement = $('#products_sold_measurement');
var unitPriceFob = $('#products_sold_unitPriceFob');
var unitPricenavlun = $('#products_sold_unitPriceNavlun');
var totalUnitPrice = $('#products_sold_totalUnitPrice');
// ÖDEME FORMU LİSTEYE EKLEME
$('#save-payment-btn').on('click', function() {
let amount = roundToDecimal(convertToDecimal($('#payment_amount').val()));
let payment_paymentStatus = $('#payment_paymentStatus').val();
let payment_paymentDate = $('#payment_paymentDate').val();
let payment_dueDate = $('#payment_dueDate').val();
let payment_paymentMethod = $('#payment_paymentMethod').val();
let payment_description = $('#payment_description').val();
if (amount <= 0 || !payment_paymentStatus || !payment_paymentDate || !payment_dueDate || !payment_paymentMethod || !payment_description) {
throw new Error("Formdaki eksikleri tamamlayın lütfen")
}
payment = {
uuid: crypto.randomUUID(),
amount: amount,
status: payment_paymentStatus,
paymentDate: payment_paymentDate,
paymentDueDate: payment_dueDate,
paymentMethod: payment_paymentMethod,
description: payment_description,
voucherCode: $('#payment_voucherCode').val()
}
payments.push(payment);
fetchPaymentForm();
// Trigger update to show AVOIR if needed
fetchSoldListRows();
});
// Ödeme satırını silme işlemi
$(document).on('click', '.delete-payment', function() {
$row = $(this).closest('tr');
let id = $row.attr("id");
payments = payments.filter(payment => payment.uuid !== id);
fetchPaymentForm();
// Trigger update to remove AVOIR if needed
fetchSoldListRows();
});
// Ödeme formunu güncelleme
function fetchPaymentForm() {
$('#payment-list-table-tbody').empty();
payments.forEach(function(val) {
var newRow = `<tr id="${val.uuid}">
<td><input type="text" class="form-control payment-amount payment-amount-input" value="${addCommas(val.amount) || ''}" name="paymentAmount[]" placeholder="Miktar"></td>
<td>
<select class="form-control" name="paymentStatus[]">
<option value="0" ${val.status === "0" ? 'selected' : ''}>{% trans %} payment_status.paid {% endtrans %}</option>
<option value="1" ${val.status === "1" ? 'selected' : ''}>{% trans %} payment_status.not_paid {% endtrans %}</option>
<option value="2" ${val.status === "2" ? 'selected' : ''}>{% trans %} payment_status.term_sale {% endtrans %}</option>
</select>
</td>
<td><input name="paymentDate[]" type="date" class="form-control" value="${val.paymentDate || ''}" required></td>
<td><input name="paymentDueDate[]" type="date" class="form-control" value="${val.paymentDueDate || ''}" required></td>
<td>
<select class="form-control" name="paymentMethod[]">
<option value="0">Ödeme Yöntemi Seçin</option>
{% for paymentMethod in paymentMethods %}
<option value="{{ paymentMethod.id }}" ${val.paymentMethod === '{{ paymentMethod.id }}' ? 'selected' : ''}>
{{ paymentMethod.name }}
</option>
{% endfor %}
</select>
</td>
<td><input type="text" class="form-control" value="${val.description || ''}" name="description[]">
<input type="hidden" name="voucherCode[]" value="${val.voucherCode || ''}">
</td>
<td><button type="button" class="btn btn-primary btn-sm save-payment-row">{% trans %} save {% endtrans %}</button></td>
<td><button type="button" class="btn btn-danger btn-sm delete-payment">{% trans %} delete {% endtrans %}</button></td>
</tr>`;
// Yeni satırı tabloya ekle
$('#payment-list-table-tbody').append(newRow);
$('#add-payment-modal').modal("hide");
resetPaymentForm();
});
calculateAndWriteDiffBetweenPaymentAndSolds();
}
function updatePaymentByUUID(uuid, updatedData) {
const paymentIndex = payments.findIndex(payment => payment.uuid === uuid);
if (paymentIndex === -1) {
console.error(`Payment with UUID ${uuid} not found!`);
return false;
}
payments[paymentIndex] = {
...payments[paymentIndex], // Mevcut veriler
...updatedData // Yeni veriler
};
fetchPaymentForm();
return true;
}
function calculateAndWriteDiffBetweenPaymentAndSolds(existingTotals = null) {
const totals = existingTotals || aggregateSoldTotals();
const saleTotalRounded = totals.grandTotalRounded;
const paymentTotal = convertToDecimal(calculateTotalPaymentAmount());
const diff = totals.grandTotal.minus(paymentTotal);
const diffRounded = diff.toDecimalPlaces(2, MONEY_ROUNDING_MODE);
$('#total-amount-span').html(addCommas(saleTotalRounded.toFixed(2)));
$('#remainder-amount-span').html(addCommas(diffRounded.toFixed(2)));
}
function resetPaymentForm() {
$('#payment_amount').val('');
$('#payment_description').val('');
$('#payment_voucherCode').val('');
$('#payment_voucherCode').closest('.form-group').hide(); // Reset visibility
}
$(document).on('click', '.save-payment-row', function() {
const $row = $(this).closest('tr');
const uuid = $row.attr('id');
const updatedData = {
amount: convertToDecimal($row.find('input[name="paymentAmount[]"]').val().replace(',', '')).toNumber(),
status: $row.find('select[name="paymentStatus[]"]').val(),
paymentDate: $row.find('input[name="paymentDate[]"]').val(),
paymentDueDate: $row.find('input[name="paymentDueDate[]"]').val(),
paymentMethod: $row.find('select[name="paymentMethod[]"]').val(),
description: $row.find('input[name="description[]"]').val(),
voucherCode: $row.find('input[name="voucherCode[]"]').val()
};
const result = updatePaymentByUUID(uuid, updatedData);
if (result) {
showSuccessAlert("Ödeme satırı güncellendi :", updatedData);
} else {
console.log("Failed to update payment.");
}
});
function resetAddProductForm() {
$('#products_sold_quantity').val(0);
$('#products_sold_totalUnitPurchasePrice').val(0);
$('#products_sold_unAllocatedQuantity').val(0);
$('#products_sold_totalPuchasePrice').val(0);
}
function calculatePrice() {
var quantityP = convertToFloat($('#products_sold_quantity').val());
var unAllocatedQuantityP = convertToFloat($('#products_sold_unAllocatedQuantity').val());
var totalUnitPurchasePriceP = convertToFloat($('#products_sold_totalUnitPurchasePrice').val());
var taxP = convertToFloat($('#products_sold_tax').val());
var discountP = convertToFloat($('#products_sold_discount').val());
var totalAmount = (quantityP + unAllocatedQuantityP) * totalUnitPurchasePriceP;
var discountedAmount = totalAmount - (totalAmount * (discountP / 100));
var finalAmount = discountedAmount + (discountedAmount * (taxP / 100));
finalAmount = roundToTwoDecimals(finalAmount);
$('#products_sold_totalPuchasePrice').val(finalAmount);
}
$(document).on('change', '#products_sold_tax', calculatePrice);
$(document).on('change', '#products_sold_discount', calculatePrice);
$(document).on('change', '#products_sold_quantity', calculatePrice);
$(document).on('change', '#products_sold_totalUnitPurchasePrice', calculatePrice);
function roundToTwoDecimals(number) {
var factor = Math.pow(10, 2);
return (Math.round(number * factor) / factor).toFixed(2);
}
function validate(s) {
var rgx = /^[0-9]*\.?[0-9]*$/;
return s.match(rgx);
}
function convertToFloat(value) {
if (typeof value === "string") {
value = value.replace(',', '');
}
return parseFloat(value);
}
$(document).on('change', 'input[name="totalUnitPurchasePrice[]"]', function() {
var unitPrice = $(this).val();
});
var productCode = 0;
$('#productselect').on('change', function() {
$.ajax({
url: "/stock-detail/" + $('#productselect option:selected').val(),
dataType: 'JSON',
method: 'GET',
success: function(response) {
var productid = response.productid;
var product = products.find(function(item) {
return item.productid === productid;
});
product.length = response.length;
product.priceFob = response.priceFob;
product.priceNavlun = response.priceNavlun;
product.totalQuantity = response.totalQuantity;
product.quantity = response.quantity;
product.unAllocatedQuantity = response.unAllocatedQuantity;
product.thickness = response.thickness;
product.totalUnitPrice = response.totalUnitPrice;
product.width = response.width;
selectProduct(product.productid);
getProductCost(product.productid);
fetchStocks();
productname.val(product.name);
productmeasurement.val(product.measurement);
productCode = product.code;
unitPriceFob.val(addCommas(product.priceFob));
unitPricenavlun.val(addCommas(product.priceNavlun));
totalUnitPrice.val(addCommas(product.priceNavlun + response.priceFob));
totalQuantity = product.totalQuantity;
productId = product.productid;
// Seçilen ürün için birimleri yükle ve ana birimi seç
fillUnitSelects(product);
computeAndShowBaseUnitEquivalent();
},
error: function(error) {
// Hata yönetimi
}
});
});
function isProductService(productId){
const product = products.find(product => product.productid === productId);
const productType = product.productType.key;
if(productType === "INSTALLATION_SERVICE" || productType === "MAINTENANCE_SERVICE" || productType === "CONSULTATION_SERVICE" || productType === 'TREE'){
return true;
}
return false;
}
// TODO
$('#add-product-row-btn').on('click', function() {
var productId = $('#productselect').val();
if (!productId || productId <= 0) {
showErrorAlert("Ürün seçimi yapın");
return;
}
let product = getSelectedProduct();
const productType = product.productType.key;
var quantity = $('#adder-allocated-quantity').val();
var unAllocatedQuantity = $('#adder-unallocated-quantity').val();
var unitPrice = $('#adder-price').val();
var tax = $('#adder-tva-select').val();
var discount = $('#adder-discount-select').val();
var totalPurchasePrice = $('#adder-total').val();
quantity = convertToDecimal(quantity);
unAllocatedQuantity = convertToDecimal(unAllocatedQuantity);
unitPrice = convertToDecimal(unitPrice);
tax = convertToDecimal(tax);
discount = convertToDecimal(discount);
totalPurchasePrice = convertToDecimal(totalPurchasePrice);
if(productType === 'TREE') {
}
checkStockQuantity(quantity);
const selectedUnitId = $('#adder-quantity-unit-select').val();
if (!selectedUnitId || String(selectedUnitId) === '') {
showErrorAlert('Lütfen birim seçiniz.');
return;
}
const item = addProductToSoldList(
product.productid,
product.name,
product.code,
product.measurement,
quantity,
unitPrice,
tax,
discount,
totalPurchasePrice,
product.warehouseid,
unAllocatedQuantity,
product.thickness,
product.width,
product.length,
product.cost,
product.productType,
selectedUnitId,
);
if(isProductService(product.productid)){
$('#update_service_id').val(item.uuid);
$('#updateServiceModal').modal('show');
}else{
if(quantity.toNumber() === 0 && unAllocatedQuantity.toNumber() === 0) {
showErrorAlert("Miktar veya tahsis edilmemiş miktar 0 olamaz");
return;
}
}
fetchSoldListRows();
resetAdderRow();
});
$('#service_modal_save_changes_btn').on('click', function() {
const uuid = $('#update_service_id').val();
const soldProduct = soldList.find(soldProduct => soldProduct.uuid === uuid);
const product = products.find(product => product.productid === soldProduct.productid);
const productType = product.productType.key;
// Modal değerlerini geri alıp objeye yazıyoruz
soldProduct.productName = $('#update_service_name').val();
soldProduct.cost = parseFloat($('#update_service_cost').val());
soldProduct.description = $('#update_service_description').val();
const tr = $('#' + uuid);
console.log("Servis güncellendi:", soldProduct);
$('#updateServiceModal').modal('hide');
// Rows'ları yeniden render et ki service adı dropdown'da görünsün
fetchSoldListRows();
updateTotalAmounts();
if(isProductService(product.productid)){
addServiceEditButtonIfNeeded(uuid);
}
});
$(document).on('click', '.edit-service-button', function () {
const uuid = $(this).data('uuid');
const soldProduct = soldList.find(sp => sp.uuid === uuid);
$('#update_service_id').val(uuid);
$('#update_service_name').val(soldProduct?.productName || "");
$('#update_service_cost').val(soldProduct?.cost || "0.00");
$('#update_service_description').val(soldProduct?.description || "");
$('#updateServiceModal').modal('show');
});
// TODO:
function addServiceEditButtonIfNeeded(uuid) {
const soldProduct = soldList.find(sp => sp.uuid === uuid);
const product = products.find(p => p.productid === soldProduct.productid);
const productType = product.productType.key;
// Sadece ürün service ise buton ekle
if (product && isProductService(product.productid)) {
const tr = $('#' + uuid);
// Eğer daha önce eklenmediyse ekle
if (tr.find('.edit-service-button').length === 0) {
const btn = `
<button type="button"
class="btn btn-warning btn-sm edit-service-button"
data-uuid="${uuid}"
title="Servisi Güncelle">
<i class="fas fa-edit"></i>
</button>`;
// örnek: sil butonunun yanına ekleyelim
tr.find('.btn-group').prepend(btn);
}
}
}
// Reset adder row form after adding product
function resetAdderRow() {
$('#productselect').val('').trigger('change');
$('#adder-allocated-quantity').val('0');
$('#adder-unallocated-quantity').val('0');
$('#adder-price').val('0');
$('#adder-discount-select').val('0');
$('#adder-tva-select').val('0');
$('#adder-total').val('0');
$('#unalloc-display').text('0');
clearUnitSelects();
$('#info-details-content').hide();
$('#show-info-btn i').removeClass('fa-chevron-up').addClass('fa-info-circle');
}
// =============================================================================
// EVENT HANDLERS FOR SALES LIST EDITING
// =============================================================================
// Show/hide product info details
$(document).on('click', '.show-row-info-btn', function() {
const $infoContent = $(this).closest('tr').next('.info-details-row-class').find('.info-details-content-class');
const $icon = $(this).find('i');
if ($infoContent.is(':visible')) {
$infoContent.hide();
$icon.removeClass('fa-chevron-up').addClass('fa-info-circle');
$(this).closest('tr').next('.info-details-row-class').hide();
} else {
$infoContent.show();
$icon.removeClass('fa-info-circle').addClass('fa-chevron-up');
$(this).closest('tr').next('.info-details-row-class').show();
}
});
$(document).on('change', '.product-select-row', function() {
const uuid = $(this).data('uuid');
const newProductId = parseInt($(this).val());
const soldItemIndex = soldList.findIndex(item => item.uuid === uuid);
if (soldItemIndex > -1) {
const newProduct = products.find(p => p.productid === newProductId);
if (newProduct) {
soldList[soldItemIndex].productid = newProduct.productid;
soldList[soldItemIndex].productName = newProduct.name;
soldList[soldItemIndex].code = newProduct.code;
soldList[soldItemIndex].measurement = newProduct.measurement;
soldList[soldItemIndex].cost = newProduct.cost;
}
fetchSoldListRows();
updateTotalAmounts();
}
console.table(soldList);
});
// Handle unit price changes in sold list rows
$(document).on('input change', '.price-input-row', function() {
const uuid = $(this).data('uuid');
const value = $(this).val();
// Birim fiyat manuel değiştirildi, artık otomatik hesaplama yapılmalı
delete rowHasManualTotal[uuid];
updateSoldListItem(uuid, 'unitPrice', value);
});
// Handle discount changes in sold list rows
$(document).on('change', '.discount-select-row', function() {
const uuid = $(this).data('uuid');
const value = $(this).val();
// İndirim değiştirildi, artık otomatik hesaplama yapılmalı
delete rowHasManualTotal[uuid];
updateSoldListItem(uuid, 'discount', value);
});
// Handle tax changes in sold list rows
$(document).on('change', '.tax-select-row', function() {
const uuid = $(this).data('uuid');
const value = $(this).val();
// KDV değiştirildi, artık otomatik hesaplama yapılmalı
delete rowHasManualTotal[uuid];
updateSoldListItem(uuid, 'tax', value);
});
// Handle unit selection changes in sold list rows
$(document).on('change', '.quantity-unit-select', function() {
const uuid = $(this).data('uuid');
const unitId = $(this).val();
const $quantityInput = $(`.quantity-input-row[data-uuid="${uuid}"]`);
const currentQuantity = parseFloat($quantityInput.val()) || 0;
// Update the selected unit in sold list
const itemIndex = soldList.findIndex(item => item.uuid === uuid);
if (itemIndex !== -1) {
const item = soldList[itemIndex];
const oldUnitId = item.selectedUnitId;
item.selectedUnitId = unitId;
// Show unit conversion equivalent
showUnitConversionForRow(uuid, item.productid, currentQuantity, unitId);
// If changing from one unit to another, convert the quantity
if (oldUnitId && oldUnitId !== unitId && unitId && currentQuantity > 0) {
convertQuantityBetweenUnits(item.productid, currentQuantity, oldUnitId, unitId, function(convertedQuantity) {
if (convertedQuantity !== null) {
$quantityInput.val(convertedQuantity.toFixed(2));
updateSoldListItem(uuid, 'quantity', convertedQuantity);
// Update conversion display with new quantity
showUnitConversionForRow(uuid, item.productid, convertedQuantity, unitId);
}
});
}
// Recompute and store base unit quantity for the row
computeBaseUnitQuantity(item.productid, unitId, parseFloat($quantityInput.val()) || 0, function(baseQty){
const idx = soldList.findIndex(sp => sp.uuid === uuid);
if(idx !== -1){ soldList[idx].baseUnitQuantity = convertToDecimal(baseQty || 0).toNumber(); }
});
}
});
// Handle quantity changes in sold list rows to update conversion display
$(document).on('input change', '.quantity-input-row', function() {
const uuid = $(this).data('uuid');
const value = $(this).val();
const item = soldList.find(item => item.uuid === uuid);
if (item && item.selectedUnitId) {
showUnitConversionForRow(uuid, item.productid, parseFloat(value) || 0, item.selectedUnitId);
}
// Miktar manuel değiştirildi, artık otomatik hesaplama yapılmalı
delete rowHasManualTotal[uuid];
updateSoldListItem(uuid, 'quantity', value);
// Also refresh baseUnitQuantity on quantity change
if (item) {
computeBaseUnitQuantity(item.productid, item.selectedUnitId, parseFloat(value) || 0, function(baseQty){
const idx = soldList.findIndex(sp => sp.uuid === uuid);
if(idx !== -1){ soldList[idx].baseUnitQuantity = convertToDecimal(baseQty || 0).toNumber(); }
});
}
});
// Show unit conversion equivalent for a specific row
function showUnitConversionForRow(uuid, productId, quantity, fromUnitId) {
if (!fromUnitId || !quantity || quantity <= 0) {
updateRowQuantityHint(uuid, '');
return;
}
const product = products.find(p => p.productid === productId);
if (!product || !product.measurementUnitId) {
updateRowQuantityHint(uuid, '');
return;
}
// If already in product's main unit, no conversion needed
if (fromUnitId == product.measurementUnitId) {
updateRowQuantityHint(uuid, '');
return;
}
$.ajax({
url: '/admin/measurement/convert-to-product-unit',
method: 'POST',
dataType: 'json',
data: {
productId: productId,
fromUnitId: fromUnitId,
quantity: quantity
},
success: function(resp) {
if (resp && resp.success) {
const unitText = product.measurementUnit ? (' ' + product.measurementUnit) : '';
updateRowQuantityHint(uuid, (Number(resp.result).toFixed(2)) + unitText);
} else {
updateRowQuantityHint(uuid, '');
}
},
error: function() {
updateRowQuantityHint(uuid, '');
}
});
}
// Update quantity hint for a specific row
function updateRowQuantityHint(uuid, text) {
let $hint = $(`.quantity-conversion-hint[data-uuid="${uuid}"]`);
if ($hint.length === 0) {
const $quantityCell = $(`.quantity-input-row[data-uuid="${uuid}"]`).closest('td');
$quantityCell.append(`<small class="form-text text-muted quantity-conversion-hint" data-uuid="${uuid}"></small>`);
$hint = $(`.quantity-conversion-hint[data-uuid="${uuid}"]`);
}
$hint.text(text || '');
}
// Convert quantity between different measurement units
function convertQuantityBetweenUnits(productId, quantity, fromUnitId, toUnitId, callback) {
if (!fromUnitId || !toUnitId || fromUnitId === toUnitId) {
callback(quantity);
return;
}
$.ajax({
url: '/admin/measurement/convert-between-units',
method: 'POST',
dataType: 'json',
data: {
productId: productId,
fromUnitId: fromUnitId,
toUnitId: toUnitId,
quantity: quantity
},
success: function(resp) {
if (resp && resp.success) {
callback(parseFloat(resp.result));
} else {
console.error('Unit conversion failed:', resp);
callback(null);
}
},
error: function() {
console.error('Unit conversion request failed');
callback(null);
}
});
}
// Handle total price changes (reverse calculation to unit price)
$(document).on('change blur', '.total-input-row', function() {
updateUnitPriceFromTotal($(this).data('uuid'));
});
// Handle delete row button clicks
const currencySymbol = "{{ 'currency.symbol'|trans|e('js') }}";
const PAYMENT_TOLERANCE = 0.01;
const AUTO_PAYMENT_DESCRIPTION = 'Sistem tarafından ödeme otomatik oluşturuldu';
const AUTO_PAYMENT_STATUS_TEXT = 'Ödendi';
let autoPaymentModalState = null;
let cachedCashPaymentMethod = null;
function detectCashPaymentMethod() {
const $options = $('#payment_paymentMethod option');
let fallback = null;
let cashCandidate = null;
$options.each(function() {
const $option = $(this);
const value = ($option.val() || '').trim();
if (!value) {
return;
}
if (!fallback) {
fallback = { id: value, label: ($option.text() || '').trim() };
}
const label = ($option.text() || '').toLowerCase();
if (label.includes('cash') || label.includes('nakit')) {
cashCandidate = { id: value, label: ($option.text() || '').trim() };
return false;
}
});
if (cashCandidate) {
return cashCandidate;
}
if (fallback) {
return fallback;
}
return { id: '1', label: 'Cash' };
}
function ensureCashPaymentMethod() {
if (!cachedCashPaymentMethod) {
cachedCashPaymentMethod = detectCashPaymentMethod();
}
return cachedCashPaymentMethod;
}
function normalizeCurrency(value) {
return convertToDecimal(value).toDecimalPlaces(2, Decimal.ROUND_HALF_UP).toNumber();
}
function formatCurrency(value) {
const normalized = normalizeCurrency(value);
const formatted = addCommas(normalized.toFixed(2));
return formatted.replace(/,/g, '#').replace(/\./g, ',').replace(/#/g, '.') + ' ' + currencySymbol;
}
function calculateTotalPaymentAmount() {
let total = new Decimal(0);
payments.forEach(function(payment) {
total = total.plus(convertToDecimal(payment.amount));
});
return total.toNumber();
}
function getSaleDateValue() {
const saleDate = $('#sales_form_salesDate').val();
if (saleDate && saleDate.trim() !== '') {
return saleDate;
}
return new Date().toISOString().split('T')[0];
}
function createAutoPaymentPayload(amount, saleDate) {
const method = ensureCashPaymentMethod();
return {
uuid: crypto.randomUUID(),
amount: normalizeCurrency(amount),
status: '0',
paymentDate: saleDate,
paymentDueDate: saleDate,
paymentMethod: method.id,
description: AUTO_PAYMENT_DESCRIPTION
};
}
function openAutoPaymentModal(config) {
autoPaymentModalState = {
scenario: config.scenario,
amount: normalizeCurrency(config.amount),
saleDate: config.saleDate,
saleTotal: normalizeCurrency(config.saleTotal),
currentPayments: normalizeCurrency(config.currentPayments)
};
const method = ensureCashPaymentMethod();
let message = '';
if (config.scenario === 'no-payment') {
message = 'Bu satış için henüz ödeme girilmedi. Aşağıdaki bilgilerle otomatik bir nakit ödeme oluşturulacaktır.';
} else {
message = 'Girilen ödemeler satış toplamı ile uyumsuz. Eksik tutar otomatik bir nakit ödeme olarak eklenecektir.';
}
$('#auto-payment-message').text(message);
$('#auto-payment-sale-total').text(formatCurrency(autoPaymentModalState.saleTotal));
$('#auto-payment-current-payments').text(formatCurrency(autoPaymentModalState.currentPayments));
$('#auto-payment-amount').text(formatCurrency(autoPaymentModalState.amount));
$('#auto-payment-method').text(method.label);
$('#auto-payment-status').text(AUTO_PAYMENT_STATUS_TEXT);
$('#auto-payment-description').text(AUTO_PAYMENT_DESCRIPTION);
$('#confirm-auto-payment').prop('disabled', false);
$('#autoPaymentModal').modal('show');
}
function validateSalesForm() {
const form = $('form[name="sales_form"]');
if (!form || form.length === 0) {
return true;
}
return form[0].reportValidity();
}
function submitSalesRequest() {
if (!validateSalesForm()) {
return;
}
const salesData = {
payments: payments,
soldList: soldList,
invoice: {
invoiceNumber: $('#sales_form_invoiceNumber').val(),
customer: $('#sales_form_customer').val(),
paymentStatus: $('#sales_form_paymentStatus').val(),
salesDate: $('#sales_form_salesDate').val(),
deliveryDate: $('#sales_form_deliveryDate').val(),
dueDate: $('#sales_form_dueDate').val(),
validDate: $('#sales_form_validDate').val(),
seller: $('#sales_form_seller').val(),
description: $('#sales_form_description').val(),
shippingNotes: $('#sales_form_shippingNotes').val(),
paymentNotes: $('#sales_form_paymentNotes').val(),
salesType: $('input[name="salesType"]').filter(':checked').val() || 'proforma',
salesStatus: $('#sales_form_status').val() ? $('#sales_form_status').val() : '0'
}
};
$.ajax({
url: "{{ path('app_admin_sales_create') }}",
method: 'POST',
data: salesData,
success: function(response) {
window.location.href = "/sales/detail/" + response.id;
Swal.fire({
icon: 'success',
title: 'Başarılı',
text: 'Satış başarıyla oluşturuldu'
});
},
error: function(xhr) {
var response = JSON.parse(xhr.responseText);
showErrorAlert(response.message);
}
});
}
function finalizeSalesSubmission() {
const saleTotal = convertToDecimal(getTotalSoldAmount());
const totalPayments = convertToDecimal(calculateTotalPaymentAmount());
const difference = saleTotal.minus(totalPayments).abs().toNumber();
if (difference > PAYMENT_TOLERANCE) {
showErrorAlert('Ödemeler satış toplamı ile hâlâ uyumsuz. Lütfen kontrol edin.');
return;
}
submitSalesRequest();
}
$('#confirm-auto-payment').on('click', function() {
if (!autoPaymentModalState) {
return;
}
const saleDate = autoPaymentModalState.saleDate || getSaleDateValue();
const autoPayment = createAutoPaymentPayload(autoPaymentModalState.amount, saleDate);
payments.push(autoPayment);
fetchPaymentForm();
calculateAndWriteDiffBetweenPaymentAndSolds();
$('#confirm-auto-payment').prop('disabled', true);
$('#autoPaymentModal').modal('hide');
autoPaymentModalState = null;
finalizeSalesSubmission();
});
$('#autoPaymentModal').on('hidden.bs.modal', function() {
$('#confirm-auto-payment').prop('disabled', false);
autoPaymentModalState = null;
});
ensureCashPaymentMethod();
$(document).on('click', '.delete-row-button', function() {
const uuid = $(this).data('uuid');
deleteProductInSoldList(uuid);
$(`tr[id="${uuid}"]`).remove();
$(`.info-details-row-class[data-uuid="${uuid}"]`).remove();
fetchStocks();
updateTotalAmounts();
fetchPaymentForm();
$('#sales_form_totalPurchasePrice').val(addCommas(getTotalSoldAmount()));
});
// Handle unallocated quantity modal for rows
$(document).on('click', '.open-unallocated-modal-row', function() {
const uuid = $(this).data('uuid');
const itemIndex = soldList.findIndex(item => item.uuid === uuid);
if (itemIndex === -1) return;
// Değeri gizli input yerine doğrudan soldList'ten oku
const currentQty = soldList[itemIndex].unAllocatedQuantity || 0;
$('#modal-unallocated-quantity').val(Number(currentQty).toFixed(2));
// Store current uuid for saving later
$('#unallocatedModal').data('current-uuid', uuid);
$('#unallocatedModal').modal('show');
});
// Save unallocated quantity from modal for rows
$(document).on('click', '#save-unallocated-btn', function() {
const uuid = $('#unallocatedModal').data('current-uuid');
// Sadece satırlar için olan bölüm (uuid varsa)
if (uuid) {
const qty = $('#modal-unallocated-quantity').val() || 0;
const newUnallocatedQty = convertToDecimal(qty).toNumber();
// 1. soldList'teki ilgili ürünü bul ve unAllocatedQuantity'yi güncelle
const itemIndex = soldList.findIndex(item => item.uuid === uuid);
if (itemIndex > -1) {
soldList[itemIndex].unAllocatedQuantity = newUnallocatedQty;
}
// 2. DOĞRU SATIRI HEDEFLEYEREK butondaki sayıyı güncelle
const $button = $(`.open-unallocated-modal-row[data-uuid="${uuid}"]`);
$button.find('.unalloc-display').text(newUnallocatedQty.toFixed(2));
// 3. Tahsis edilmemiş miktar değiştirildi, artık otomatik hesaplama yapılmalı
delete rowHasManualTotal[uuid];
// 4. Satırın genel toplamlarını ve stokları güncelle
updateSoldProductFromRow(uuid);
$('#unallocatedModal').modal('hide');
} else { // Bu kısım adder-row için, dokunmuyoruz
const qty = $('#modal-unallocated-quantity').val() || 0;
$('#adder-unallocated-quantity').val(qty).trigger('change');
$('#unalloc-display').text(qty);
onInputChange.call($('#adder-unallocated-quantity')[0]);
$('#unallocatedModal').modal('hide');
}
});
// Flag to prevent circular updates between unit price and total
let isUpdatingFromTotal = false;
let isUpdatingFromUnitPrice = false;
// adder-row input alanlarının değişikliklerini dinleme
$('#adder-row').on('input change',
'#adder-allocated-quantity, ' +
'#adder-unallocated-quantity, ' +
'#adder-price, ' +
'#adder-discount-select, ' +
'#adder-tva-select, ' +
'#adder-total', onInputChange);
// Birim dönüşümünü yazma bittiğinde (blur) ve birim seçiminde (change) yap
$('#adder-row').on('blur', '#adder-allocated-quantity', function(){
computeAndShowBaseUnitEquivalent();
});
$('#adder-row').on('change', '#adder-quantity-unit-select', function(){
computeAndShowBaseUnitEquivalent();
});
// Adder rowdaki input alanlarının değişikliklerini dinleme
function onInputChange() {
if($(this).attr('id') === 'adder-total') {
// Toplam manuel değiştirildi → birim fiyatı hesapla, ama toplamı tekrar değiştirme
if(isUpdatingFromUnitPrice) return;
isUpdatingFromTotal = true;
const totalAmount = convertToDecimal($('#adder-total').val() || 0).toNumber();
const allocatedQuantity = convertToDecimal($('#adder-allocated-quantity').val() || 0).toNumber();
const unallocatedQuantity = convertToDecimal($('#adder-unallocated-quantity').val() || 0).toNumber();
const discountPercentage = convertToDecimal((($('#adder-discount-select').val() || '0') + '').replace('%', '')).toNumber();
const tvaPercentage = convertToDecimal((($('#adder-tva-select').val() || '0') + '').replace('%', '')).toNumber();
const unitPrice = calculateUnitPrice(totalAmount, allocatedQuantity, unallocatedQuantity, tvaPercentage, discountPercentage);
$('#adder-price').val(convertToDecimal(unitPrice).toDecimalPlaces(2, MONEY_ROUNDING_MODE).toFixed(2));
setTimeout(function(){ isUpdatingFromTotal = false; }, 0);
} else {
// Birim fiyat, miktar, indirim veya KDV değişti → toplamı hesapla
if(isUpdatingFromTotal) return;
isUpdatingFromUnitPrice = true;
const allocatedQuantity = convertToDecimal($('#adder-allocated-quantity').val() || 0).toNumber();
const unallocatedQuantity = convertToDecimal($('#adder-unallocated-quantity').val() || 0).toNumber();
const price = convertToDecimal($('#adder-price').val() || 0).toNumber();
const discountPercentage = convertToDecimal((($('#adder-discount-select').val() || '0') + '').replace('%', '')).toNumber();
const tvaPercentage = convertToDecimal((($('#adder-tva-select').val() || '0') + '').replace('%', '')).toNumber();
calculateTotal(allocatedQuantity, unallocatedQuantity, price, tvaPercentage, discountPercentage);
setTimeout(function(){ isUpdatingFromUnitPrice = false; }, 0);
}
}
function roundToNearestCent(amount) {
return Math.round(amount * 100) / 100;
}
// Toplam tutarı hesaplama
// Her ara adımda virgülden sonra 2 hane gösterilecek, 3. hane 5 ve üzeri ise yukarı yuvarlanacak
function calculateTotal(allocatedQuantity, unallocatedQuantity, price, tvaPercentage, discountPercentage) {
const totals = calculateRowFinancials({
quantity: allocatedQuantity,
unAllocatedQuantity: unallocatedQuantity,
unitPrice: price,
discount: discountPercentage,
tax: tvaPercentage
});
$('#adder-total').val(totals.totalRounded.toFixed(2));
}
// Toplam tutardan Birim fiyatı hesaplama
function calculateUnitPrice(totalAmount, allocatedQuantity, unallocatedQuantity, tvaPercentage, discountPercentage) {
const totalQuantity = convertToDecimal(allocatedQuantity || 0).plus(convertToDecimal(unallocatedQuantity || 0));
if (totalQuantity.isZero()) {
return 0;
}
const total = convertToDecimal(totalAmount || 0);
// Eğer toplam 0 ise, birim fiyat da 0 olmalı
if (total.isZero()) {
return 0;
}
const discountMultiplier = new Decimal(1).minus(convertToDecimal(discountPercentage || 0).div(100));
const taxMultiplier = new Decimal(1).plus(convertToDecimal(tvaPercentage || 0).div(100));
// %100 indirimde discountMultiplier = 0 olur, bu durumda birim fiyat hesaplanamaz
// Çünkü total = (unitPrice * quantity * (1 - discount) * (1 + tax))
// discount = 100% ise, unitPrice = 0 olmalı (ya da sonsuz, ama mantıklı olan 0)
if (discountMultiplier.isZero()) {
return 0;
}
if (taxMultiplier.isZero()) {
return 0;
}
// Formül: unitPrice = total / (quantity * (1 - discount%) * (1 + tax%))
const subtotal = total.div(taxMultiplier).div(discountMultiplier);
const unitPrice = subtotal.div(totalQuantity);
return unitPrice.toDecimalPlaces(6, MONEY_ROUNDING_MODE).toNumber();
}
function getProductCost(pid) {
// Popup için URL burada oluşturuluyor.
const url = '/product-cost/' + pid + '/' + getSelectedProduct().warehouseid;
$.ajax({
url: "/product-cost-detail/" + pid + '?warehouse=' + getSelectedProduct().warehouseid,
method: "POST",
dataType: "JSON",
success: function(data) {
var product = getSelectedProduct();
product.cost = data.cost;
product.measurementUnit = data.measurement;
$('.stock-info-span').html(data.stock.toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " " + data.measurement);
$('.cost-info-span').html(data.cost.toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " €"); // Eski detay butonu kaldırıldı
$('.measurement-unit-info-span').html(data.measurement);
$('#hidden-cost-input').val(data.cost);
// "Maliyet" popup açıyor maliyet detaylarını gösteriyor.
const detailsHtml = `
<div class="row align-items-center">
<div class="col-md-3">${product.name}</div>
<div class="col-md-3">
<strong>
<button type="button" class="btn btn-link btn-sm p-0 cost-popup-btn" data-url="${url}">Maliyet: ${data.cost.toFixed(2)} €</button>
</strong>
</div>
<div class="col-md-3"><strong>Stok:</strong> ${data.stock.toFixed(2)} ${data.measurement}</div>
<div class="col-md-3"><strong>Birim:</strong> ${data.measurement}</div>
</div>
`;
$('#info-details-content').html(detailsHtml);
return data;
},
error: function(data) {
$('#info-details-content').html('<div class="text-danger">Ürün maliyet bilgileri yüklenemedi.</div>');
}
});
}
$(document).on('click', '.cost-popup-btn', function() {
const url = $(this).data('url');
if (url) {
createPopup(url, 'cost-detail-window');
}
});
// Unallocated modal açıldığında mask'i uygula
$('#unallocatedModal').on('shown.bs.modal', function() {
applyMasks();
});
// Unallocated modal aç/kapat ve değerleri senkronize et
$(document).on('click', '#open-unallocated-modal', function() {
// Mevcut değerleri moda aktarma
const currentQty = $('#adder-unallocated-quantity').val() || 0;
const currentUnit = $('#adder-unallocated-quantity-unit-select').val() || '';
$('#modal-unallocated-quantity').val(Number(currentQty).toFixed(2));
// Seçili ürünün saklanan birimlerini modal select'ine doldur
const selectedProduct = getSelectedProduct();
if (selectedProduct && selectedProduct.convertibleUnits) {
populateSelectWithUnits($('#modal-unallocated-unit'), selectedProduct.convertibleUnits);
} else {
$('#modal-unallocated-unit').empty().append('<option value="">-</option>');
}
$('#modal-unallocated-unit').val(currentUnit);
$('#unallocatedModal').modal('show');
});
// 'Tahsis Edilmemiş Miktar' Modal'ındaki Kaydet Butonunun Düzeltilmiş Hali
$(document).on('click', '#save-unallocated-btn', function() {
const uuid = $('#unallocatedModal').data('current-uuid');
const qty = $('#modal-unallocated-quantity').val() || 0;
const newUnallocatedQty = convertToDecimal(qty).toNumber();
// DURUM 1: Satış listesindeki bir satır güncelleniyor (uuid mevcut)
if (uuid) {
// 1. soldList'teki ilgili ürünü bul ve unAllocatedQuantity'yi güncelle
const itemIndex = soldList.findIndex(item => item.uuid === uuid);
if (itemIndex > -1) {
soldList[itemIndex].unAllocatedQuantity = newUnallocatedQty;
}
// 2. Sadece ilgili satırı ID ile hedefle ve içindeki göstergeyi güncelle
const $row = $(`#${uuid}`);
$row.find('.unalloc-display').text(newUnallocatedQty.toFixed(2));
// 3. Satırın genel toplamlarını ve stokları güncelle
updateSoldProductFromRow(uuid);
}
// DURUM 2: Adder-row (ekleme satırı) güncelleniyor (uuid mevcut değil)
else {
// Sadece adder-row'daki gizli input'u ve göstergeyi güncelle
$('#adder-unallocated-quantity').val(newUnallocatedQty).trigger('change');
$('#unalloc-display').text(newUnallocatedQty.toFixed(2));
// Adder-row'daki toplamı yeniden hesapla
onInputChange.call($('#adder-unallocated-quantity')[0]);
}
// İşlem tamamlandıktan sonra modalı kapat
$('#unallocatedModal').modal('hide');
// Modal'daki uuid verisini temizle ki bir sonraki açılışta karışmasın
$('#unallocatedModal').removeData('current-uuid');
});
function formatState(state) {
if (!state.id) {
return state.text;
}
var baseUrl = "/images";
var $state = $(
'<span><img src="' + baseUrl + '/' + state.element.dataset.image + '" class="img-flag list-image" style="width: 50px;"/> ' + state.text + '</span>'
);
return $state;
};
$('#productselect').select2({
theme: 'bootstrap4',
dropdownCssClass: 'custom-select-dropdown',
placeholder: 'Ürün Seçin',
allowClear: true
});
function showErrorAlert(message) {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: '' + message,
background: 'white',
});
}
window.addEventListener("error", function(event) {
showErrorAlert(event.message);
});
function showSuccessAlert(message) {
Swal.fire({
icon: 'success',
title: 'Başarılı',
text: '' + message,
background: 'white',
});
}
$('#submitbtn').on('click', function(event) {
event.preventDefault();
if (soldList.length === 0) {
showErrorAlert("{% trans %} sale.errors.cannot_save_without_products {% endtrans %}");
return;
}
if (!validateSalesForm()) {
return;
}
const saleTotalValue = convertToDecimal(getTotalSoldAmount()).toNumber();
const currentPaymentTotal = convertToDecimal(calculateTotalPaymentAmount()).toNumber();
const saleDate = getSaleDateValue();
if (payments.length === 0 && saleTotalValue > PAYMENT_TOLERANCE) {
openAutoPaymentModal({
scenario: 'no-payment',
amount: saleTotalValue,
saleTotal: saleTotalValue,
currentPayments: currentPaymentTotal,
saleDate: saleDate
});
return;
}
const diffValue = convertToDecimal(saleTotalValue).minus(convertToDecimal(currentPaymentTotal)).toNumber();
if (Math.abs(diffValue) > PAYMENT_TOLERANCE) {
if (diffValue < 0) {
showErrorAlert('Girilen ödeme tutarı satış tutarını aşıyor. Lütfen ödeme kayıtlarını kontrol edin.');
return;
}
openAutoPaymentModal({
scenario: 'mismatch',
amount: diffValue,
saleTotal: saleTotalValue,
currentPayments: currentPaymentTotal,
saleDate: saleDate
});
return;
}
submitSalesRequest();
});
function addCommas(number) {
let decimals = 2;
number = (number + '').replace(/[^0-9+\-Ee.]/g, '');
var n = !isFinite(+number) ? 0 : +number;
var prec = !isFinite(+decimals) ? 0 : Math.abs(decimals);
var sep = ',';
var dec = '.';
var s = '';
var toFixedFix = function(n, prec) {
var k = Math.pow(10, prec);
return '' + (Math.round(n * k) / k).toFixed(prec);
};
s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
if (s[0].length > 3) {
s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
}
if ((s[1] || '').length < prec) {
s[1] = s[1] || '';
s[1] += new Array(prec - s[1].length + 1).join('0');
}
return s.join(dec);
}
$(document).on('input change', '#products_sold_totalUnitPurchasePrice, #products_sold_quantity, #products_sold_unAllocatedQuantity, #products_sold_discount, #products_sold_tax', function() {
var purchasePrice = $('#products_sold_totalUnitPurchasePrice').val() || "0";
var purchasePriceClean = parseFloat(purchasePrice.replace(/,/g, "")) || 0;
var cost = parseFloat($('#hidden-cost-input').val()) || 0;
var quantity = $('#products_sold_quantity').val() || "0";
var quantityClean = parseFloat(quantity.replace(/,/g, "")) || 0;
var unAllocatedQuantity = $('#products_sold_unAllocatedQuantity').val() || "0";
var unAllocatedQuantityClean = parseFloat(unAllocatedQuantity.replace(/,/g, "")) || 0;
var discountPercentage = $('#products_sold_discount').val() || "0";
var discountPercentageClean = parseFloat(discountPercentage.replace(/,/g, "")) || 0;
var taxPercentage = $('#products_sold_tax').val() || "0";
var taxPercentageClean = parseFloat(taxPercentage.replace(/,/g, "")) || 0;
var totalQuantity = quantityClean + unAllocatedQuantityClean;
var totalSalePriceBeforeDiscount = purchasePriceClean * totalQuantity;
var discountAmount = (totalSalePriceBeforeDiscount * discountPercentageClean) / 100;
var totalSalePriceAfterDiscount = totalSalePriceBeforeDiscount - discountAmount;
var taxAmount = (totalSalePriceAfterDiscount * taxPercentageClean) / 100;
var totalSalePrice = totalSalePriceAfterDiscount + taxAmount;
$('#products_sold_totalSalePrice').val(addCommas(totalSalePrice.toFixed(2)));
$('#products_sold_totalPuchasePrice').val(addCommas(totalSalePrice.toFixed(2)));
cost = roundToDecimal(cost);
var salePriceWithoutTax = totalSalePrice / (1 + taxPercentageClean / 100);
var totalCost = totalQuantity * cost;
var profit = salePriceWithoutTax - totalCost;
var profitSpan;
if (profit > 0) {
profitSpan = "<span style='color:green'>" + profit.toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " €</span>";
} else {
profitSpan = "<span style='color:red'>" + profit.toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " €</span>";
}
$('.profit-info-span').html(profitSpan);
});
$('#payment_amount').mask("#,##0.00", {
reverse: true
});
// Show/Hide Voucher Code field based on Payment Method
$('#payment_paymentMethod').on('change', function() {
var selectedText = $(this).find("option:selected").text().trim().toLowerCase();
var $voucherField = $('#payment_voucherCode');
var $voucherLabel = $('label[for="payment_voucherCode"]');
if (selectedText.includes('hediye') || selectedText.includes('gift') || selectedText.includes('çek')) {
$voucherField.show();
$voucherLabel.show();
$voucherField.attr('required', 'required');
} else {
$voucherField.hide();
$voucherLabel.hide();
$voucherField.val('');
$voucherField.removeAttr('required');
}
});
$('#sales_form_customer').select2({
theme: 'bootstrap4',
dropdownCssClass: 'custom-select-dropdown',
placeholder: 'Müşteri Seçin',
allowClear: true
});
$('.js-edit-customer').on('click', function() {
let customerId = $('#sales_form_customer').val();
if (!customerId) {
showErrorAlert("{% trans %} selectCustomerForEdit {% endtrans %}");
} else {
$.ajax({
url: '/admin/customer/detail/' + customerId,
type: 'GET',
dataType: 'json',
success: function(response) {
showUpdateModal(customerId, response);
},
error: function(jqXHR, textStatus, errorThrown) {
if (jqXHR.status === 404) {
showErrorAlert("Seçilen müşteri veritabanında bulunamadı.");
} else {
showErrorAlert('Müşteri bilgileri yüklenirken bir hata oluştu.');
}
clearCustomerFields();
}
});
}
});
$('#sales_form_customer').on('change', function() {
let customerId = $(this).val();
if (!customerId) {
clearCustomerFields();
return;
}
$.ajax({
url: '/admin/customer/detail/' + customerId,
type: 'GET',
dataType: 'json',
success: function(response) {
if (!response.phone || !response.email || !response.address) {
showUpdateModal(customerId, response);
} else {
populateCustomerFields(response);
}
},
error: function(jqXHR, textStatus, errorThrown) {
if (jqXHR.status === 404) {
showErrorAlert("Seçilen müşteri veritabanında bulunamadı.");
} else {
showErrorAlert('Müşteri bilgileri yüklenirken bir hata oluştu.');
}
clearCustomerFields();
}
});
});
function populateCustomerFields(customer) {
$('#update_customer_fullName').val(customer.fullName);
$('#update_customer_email').val(customer.email);
$('#update_customer_phone').val(customer.phone);
$('#update_customer_address').val(customer.address);
}
function clearCustomerFields() {
$('#update_customer_fullName, #update_customer_email, #update_customer_phone, #update_customer_address').val('');
}
function showUpdateModal(id, customer) {
$('#update_customer_id').val(id);
$('#update_customer_email').val(customer.email || '');
$('#update_customer_phone').val(customer.phone || '');
$('#update_customer_address').val(customer.address || '');
$('#update_customer_fullName').val(customer.fullName || '');
$('#updateCustomerModal').modal('show');
}
$('#updateCustomerForm').on('submit', function(e) {
e.preventDefault();
let customerId = $('#update_customer_id').val();
let formData = {
email: $('#update_customer_email').val(),
phone: $('#update_customer_phone').val(),
address: $('#update_customer_address').val(),
fullName: $('#update_customer_fullName').val()
};
$.ajax({
url: '/admin/customer/update/' + customerId,
type: 'POST',
data: formData,
success: function(response) {
$('#updateCustomerModal').modal('hide');
populateCustomerFields(response);
showSuccessAlert("{% trans %} customerUpdateSuccess {% endtrans %}");
},
error: function(jqXHR, textStatus, errorThrown) {
let errorMessage = 'Güncelleme sırasında bilinmeyen bir hata oluştu.';
try {
const response = JSON.parse(jqXHR.responseText);
if (response && response.message) {
errorMessage = response.message;
}
} catch (e) {
console.error("JSON Parse Error:", e);
console.error("Server Response:", jqXHR.responseText);
}
showErrorAlert(errorMessage);
}
});
});
$('#show-info-btn').on('click', function(e) {
e.preventDefault();
const selectedProductId = $('#productselect').val();
if (!selectedProductId || selectedProductId <= 0) {
showErrorAlert("Detayları görmek için önce bir ürün seçmelisiniz.");
return;
}
const $infoContent = $('#info-details-content');
const $icon = $(this).find('i');
$infoContent.toggle();
if ($infoContent.is(':visible')) {
$icon.removeClass('fa-info-circle').addClass('fa-chevron-up');
} else {
$icon.removeClass('fa-chevron-up').addClass('fa-info-circle');
}
});
// --- GIFT VOUCHER LOGIC ---
// Hediye Kuponu Butonuna Tıklama (Desktop)
$('#btn-gift-voucher').on('click', function() {
openGiftVoucherModal();
});
// Hediye Kuponu Butonuna Tıklama (Mobile)
$('#btn-gift-voucher-mobile').on('click', function() {
openGiftVoucherModal();
});
function openGiftVoucherModal() {
const customerId = $('#sales_form_customer').val();
if (!customerId) {
showErrorAlert('Lütfen önce bir müşteri seçiniz.');
return;
}
// API'den bakiye sorgula
$.ajax({
url: '/admin/gift-certificates/api/balance/' + customerId,
method: 'GET',
success: function(response) {
if (response.error) {
showErrorAlert(response.error);
return;
}
$('#voucher-total-balance').text(response.formatted);
$('#voucher-total-balance').data('balance', response.balance);
$('#voucher-customer-name').text(response.customerName); // Set customer name
$('#voucher-use-amount').val(''); // Inputu temizle
// Varsayılan olarak kalanı doldurmayı deneyebiliriz ama kullanıcı girişi daha iyi
const totals = aggregateSoldTotals();
const paymentTotal = convertToDecimal(calculateTotalPaymentAmount());
const remaining = totals.grandTotal.minus(paymentTotal);
if (remaining.greaterThan(0)) {
// Eğer kalan miktar, bakiyeden küçükse kalanı, büyükse bakiyeyi öner
const balance = new Decimal(response.balance);
const suggested = remaining.lessThan(balance) ? remaining : balance;
$('#voucher-use-amount').val(suggested.toNumber());
}
$('#gift-voucher-usage-modal').modal('show');
},
error: function() {
showErrorAlert('Müşteri hediye çeki bakiyesi sorgulanırken bir hata oluştu.');
}
});
}
// Hediye Çeki Kullanımını Onayla
$('#btn-confirm-voucher-usage').on('click', function() {
const amount = parseFloat($('#voucher-use-amount').val());
const balance = parseFloat($('#voucher-total-balance').data('balance'));
if (isNaN(amount) || amount <= 0) {
showErrorAlert('Lütfen geçerli bir miktar giriniz.');
return;
}
if (amount > balance) {
showErrorAlert('Girdiğiniz miktar, toplam bakiyeden fazla olamaz.');
return;
}
// Ödeme Yöntemini Bul (Hediye Çeki / Gift Voucher)
let voucherMethodId = null;
// 1. Yöntem: Backend'den gelen veriyi kullan (En Sağlam)
// 1. Yöntem: Backend'den gelen veriyi kullan (En Sağlam)
{% if paymentMethods is defined %}
const methods = [
{% for pm in paymentMethods %}
{ id: "{{ pm.id }}", name: "{{ pm.name|e('js') }}" },
{% endfor %}
];
const voucherMethod = methods.find(m =>
(m.name && m.name.toLowerCase().includes('hediye')) ||
(m.name && m.name.toLowerCase().includes('gift')) ||
(m.name && m.name.toLowerCase().includes('kupon'))
);
if (voucherMethod) {
voucherMethodId = voucherMethod.id;
}
{% endif %}
// 2. Yöntem: Eğer yukarıdaki çalışmazsa DOM'dan bulmayı dene
if (!voucherMethodId) {
const $methodSelect = $('select[name="paymentMethod[]"]').first(); // Tablodaki herhangi bir select
$methodSelect.find('option').each(function() {
const text = $(this).text().toLowerCase();
if (text.includes('hediye') || text.includes('gift') || text.includes('kupon')) {
voucherMethodId = $(this).val();
return false; // break
}
});
}
// 3. Yöntem: Modal Select
if (!voucherMethodId) {
const $modalSelect = $('#add-payment-modal select[name="paymentMethod"]');
$modalSelect.find('option').each(function() {
const text = $(this).text().toLowerCase();
if (text.includes('hediye') || text.includes('gift') || text.includes('kupon')) {
voucherMethodId = $(this).val();
return false; // break
}
});
}
if (!voucherMethodId) {
// Eğer modal select'inde yoksa, bir de manuel tanımlı ID varsa onu kontrol et (opsiyonel)
// Veya uyarı ver
// Fallback: İlk metodun id'sini alma riskine girmeyelim, kullanıcıya soralım veya hata verelim.
// Ancak genelde bir "Hediye Çeki" metodu olmalı.
// Şimdilik hata verelim.
showErrorAlert('Sistemde "Hediye Çeki" ödeme yöntemi bulunamadı. Lütfen yönetim panelinden ekleyiniz.');
return;
}
// Yeni ödeme satırı ekle
const payment = {
uuid: crypto.randomUUID(),
amount: amount,
status: "0", // Ödendi
paymentDate: new Date().toISOString().split('T')[0], // Bugün
paymentDueDate: new Date().toISOString().split('T')[0], // Bugün
paymentMethod: voucherMethodId,
description: 'Hediye Çeki Kullanımı',
voucherCode: 'AUTO_DEDUCT_FROM_BALANCE' // Backend bunu işleyip ilgili çeklerden düşebilir
};
payments.push(payment);
fetchPaymentForm();
fetchSoldListRows(); // Trigger AVOIR row update
$('#gift-voucher-usage-modal').modal('hide');
showSuccessAlert(amount + ' TL hediye çeki ödemesi eklendi.');
});
// calculateTotalPaymentAmount fonksiyonunu global scope'ta bulamadıysak buraya ekleyelim veya mevcut olanı kullanalım
function calculateTotalPaymentAmount() {
let total = new Decimal(0);
payments.forEach(function(p) {
total = total.plus(convertToDecimal(p.amount));
});
return total;
}
// --- END GIFT VOUCHER LOGIC ---
});
function createPopup(url, window_name = 'example-popup') {
var width = 1100;
var height = 700;
var screen_width = window.screen.width;
var screen_height = window.screen.height;
var popup_left = (screen_width - width) / 2;
var popup_top = (screen_height - height) / 2;
var popup_window = window.open(url, window_name, 'width=' + width + ',height=' + height + ',left=' + popup_left + ',top=' + popup_top);
}
</script>
{% endblock %}