| 1 | @model StockMaster.ViewModels.SaleCreateViewModel
|
|---|
| 2 | @{
|
|---|
| 3 | ViewData["Title"] = "New Sale";
|
|---|
| 4 | }
|
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 | @section Styles {
|
|---|
| 8 | <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
|---|
| 9 | <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" />
|
|---|
| 10 | <style>
|
|---|
| 11 |
|
|---|
| 12 | .select2-container--bootstrap-5 .select2-selection {
|
|---|
| 13 | border-color: #dee2e6;
|
|---|
| 14 | }
|
|---|
| 15 |
|
|---|
| 16 | .is-invalid + .select2-container .select2-selection {
|
|---|
| 17 | border-color: #dc3545;
|
|---|
| 18 | }
|
|---|
| 19 | </style>
|
|---|
| 20 | }
|
|---|
| 21 |
|
|---|
| 22 | <div class="row mb-4">
|
|---|
| 23 | <div class="col-12">
|
|---|
| 24 | <h2><i class="fas fa-shopping-cart"></i> Create New Sale</h2>
|
|---|
| 25 | <nav aria-label="breadcrumb">
|
|---|
| 26 | <ol class="breadcrumb">
|
|---|
| 27 | <li class="breadcrumb-item"><a href="/">Home</a></li>
|
|---|
| 28 | <li class="breadcrumb-item"><a href="/Sale/Index">Sales</a></li>
|
|---|
| 29 | <li class="breadcrumb-item active">New Sale</li>
|
|---|
| 30 | </ol>
|
|---|
| 31 | </nav>
|
|---|
| 32 | </div>
|
|---|
| 33 | </div>
|
|---|
| 34 |
|
|---|
| 35 | <form id="saleForm" method="post" action="/Sale/Create">
|
|---|
| 36 | @Html.AntiForgeryToken()
|
|---|
| 37 | <div class="row">
|
|---|
| 38 | <div class="col-md-8">
|
|---|
| 39 | <div class="card mb-3">
|
|---|
| 40 | <div class="card-header">
|
|---|
| 41 | <i class="fas fa-info-circle"></i> Sale Information
|
|---|
| 42 | </div>
|
|---|
| 43 | <div class="card-body">
|
|---|
| 44 | <div class="row">
|
|---|
| 45 | <div class="col-md-6 mb-3">
|
|---|
| 46 | <label class="form-label">Customer</label>
|
|---|
| 47 |
|
|---|
| 48 | <select name="CustomerId" id="customerSelect" class="form-select">
|
|---|
| 49 | <option value="">Select customer (optional)</option>
|
|---|
| 50 | @foreach (var customer in ViewBag.Customers)
|
|---|
| 51 | {
|
|---|
| 52 |
|
|---|
| 53 | <option value="@customer.CustomerId">@customer.Name (@customer.Email)</option>
|
|---|
| 54 | }
|
|---|
| 55 | </select>
|
|---|
| 56 | </div>
|
|---|
| 57 |
|
|---|
| 58 | <div class="col-md-6 mb-3">
|
|---|
| 59 | <label class="form-label">Warehouse *</label>
|
|---|
| 60 | <select name="WarehouseId" id="warehouseSelect" class="form-select" required>
|
|---|
| 61 | <option value="">Select warehouse</option>
|
|---|
| 62 | @foreach (var warehouse in ViewBag.Warehouses)
|
|---|
| 63 | {
|
|---|
| 64 | <option value="@warehouse.WarehouseId">@warehouse.Name</option>
|
|---|
| 65 | }
|
|---|
| 66 | </select>
|
|---|
| 67 | </div>
|
|---|
| 68 | </div>
|
|---|
| 69 | </div>
|
|---|
| 70 | </div>
|
|---|
| 71 |
|
|---|
| 72 | <div class="card">
|
|---|
| 73 | <div class="card-header d-flex justify-content-between align-items-center">
|
|---|
| 74 | <span><i class="fas fa-list"></i> Sale Items</span>
|
|---|
| 75 | <button type="button" class="btn btn-sm btn-success" onclick="addItem()">
|
|---|
| 76 | <i class="fas fa-plus"></i> Add Product
|
|---|
| 77 | </button>
|
|---|
| 78 | </div>
|
|---|
| 79 | <div class="card-body">
|
|---|
| 80 | <div id="itemsContainer">
|
|---|
| 81 |
|
|---|
| 82 | </div>
|
|---|
| 83 |
|
|---|
| 84 | @if ((ViewBag.Products == null || ViewBag.Products.Count == 0))
|
|---|
| 85 | {
|
|---|
| 86 | <div class="alert alert-warning">
|
|---|
| 87 | <i class="fas fa-exclamation-triangle"></i>
|
|---|
| 88 | No products available. Please add products first.
|
|---|
| 89 | </div>
|
|---|
| 90 | }
|
|---|
| 91 | </div>
|
|---|
| 92 | </div>
|
|---|
| 93 | </div>
|
|---|
| 94 |
|
|---|
| 95 | <div class="col-md-4">
|
|---|
| 96 | <div class="card">
|
|---|
| 97 | <div class="card-header">
|
|---|
| 98 | <i class="fas fa-calculator"></i> Summary
|
|---|
| 99 | </div>
|
|---|
| 100 | <div class="card-body">
|
|---|
| 101 | <div class="mb-3">
|
|---|
| 102 | <h4>Total: <span id="totalAmount" class="text-primary">0.00 MKD</span></h4>
|
|---|
| 103 | </div>
|
|---|
| 104 | <hr>
|
|---|
| 105 | <div class="d-grid gap-2">
|
|---|
| 106 | <button type="submit" class="btn btn-success btn-lg">
|
|---|
| 107 | <i class="fas fa-check"></i> Complete Sale
|
|---|
| 108 | </button>
|
|---|
| 109 | <a href="/Sale/Index" class="btn btn-secondary">
|
|---|
| 110 | <i class="fas fa-times"></i> Cancel
|
|---|
| 111 | </a>
|
|---|
| 112 | </div>
|
|---|
| 113 | </div>
|
|---|
| 114 | </div>
|
|---|
| 115 | </div>
|
|---|
| 116 | </div>
|
|---|
| 117 | </form>
|
|---|
| 118 |
|
|---|
| 119 |
|
|---|
| 120 | <div id="itemTemplate" style="display: none;">
|
|---|
| 121 | <div class="row mb-3 item-row">
|
|---|
| 122 | <div class="col-md-5">
|
|---|
| 123 | <label class="form-label">Product</label>
|
|---|
| 124 |
|
|---|
| 125 | <select class="form-select product-select" required>
|
|---|
| 126 | <option value="">Select product</option>
|
|---|
| 127 | @foreach (var product in ViewBag.Products)
|
|---|
| 128 | {
|
|---|
| 129 | <option value="@product.ProductId" data-price="@product.UnitPrice">
|
|---|
| 130 | @product.Name (@product.UnitPrice.ToString("N2") MKD) - SKU: @product.Sku
|
|---|
| 131 | </option>
|
|---|
| 132 | }
|
|---|
| 133 | </select>
|
|---|
| 134 | </div>
|
|---|
| 135 | <div class="col-md-3">
|
|---|
| 136 | <label class="form-label">Quantity</label>
|
|---|
| 137 | <input type="number" class="form-control quantity-input" min="1" value="1" required />
|
|---|
| 138 | </div>
|
|---|
| 139 | <div class="col-md-3">
|
|---|
| 140 | <label class="form-label">Unit Price</label>
|
|---|
| 141 | <input type="number" class="form-control price-input" step="0.01" min="0" required />
|
|---|
| 142 | </div>
|
|---|
| 143 | <div class="col-md-1">
|
|---|
| 144 | <label class="form-label"> </label>
|
|---|
| 145 | <button type="button" class="btn btn-danger w-100" onclick="removeItem(this)">
|
|---|
| 146 | <i class="fas fa-times"></i>
|
|---|
| 147 | </button>
|
|---|
| 148 | </div>
|
|---|
| 149 | </div>
|
|---|
| 150 | </div>
|
|---|
| 151 |
|
|---|
| 152 | @section Scripts {
|
|---|
| 153 |
|
|---|
| 154 | <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
|---|
| 155 |
|
|---|
| 156 | <script>
|
|---|
| 157 |
|
|---|
| 158 | $(document).ready(function() {
|
|---|
| 159 |
|
|---|
| 160 | $('#customerSelect').select2({
|
|---|
| 161 | theme: 'bootstrap-5',
|
|---|
| 162 | width: '100%',
|
|---|
| 163 | placeholder: 'Search for a customer...'
|
|---|
| 164 | });
|
|---|
| 165 |
|
|---|
| 166 |
|
|---|
| 167 | $('#warehouseSelect').select2({
|
|---|
| 168 | theme: 'bootstrap-5',
|
|---|
| 169 | width: '100%',
|
|---|
| 170 | placeholder: 'Select warehouse'
|
|---|
| 171 | });
|
|---|
| 172 |
|
|---|
| 173 |
|
|---|
| 174 | addItem();
|
|---|
| 175 | });
|
|---|
| 176 |
|
|---|
| 177 | function addItem() {
|
|---|
| 178 | const template = document.getElementById('itemTemplate').innerHTML;
|
|---|
| 179 | const container = document.getElementById('itemsContainer');
|
|---|
| 180 | const itemDiv = document.createElement('div');
|
|---|
| 181 | itemDiv.innerHTML = template;
|
|---|
| 182 | const row = itemDiv.firstElementChild;
|
|---|
| 183 |
|
|---|
| 184 | container.appendChild(row);
|
|---|
| 185 |
|
|---|
| 186 |
|
|---|
| 187 |
|
|---|
| 188 | const $productSelect = $(row).find('.product-select');
|
|---|
| 189 | const quantityInput = row.querySelector('.quantity-input');
|
|---|
| 190 | const priceInput = row.querySelector('.price-input');
|
|---|
| 191 |
|
|---|
| 192 |
|
|---|
| 193 | $productSelect.select2({
|
|---|
| 194 | theme: 'bootstrap-5',
|
|---|
| 195 | width: '100%',
|
|---|
| 196 | placeholder: 'Search product...',
|
|---|
| 197 | dropdownParent: $(row)
|
|---|
| 198 | });
|
|---|
| 199 |
|
|---|
| 200 |
|
|---|
| 201 | $productSelect.on('select2:select change', function(e) {
|
|---|
| 202 |
|
|---|
| 203 | const selectedOption = $(this).find(':selected');
|
|---|
| 204 | const price = selectedOption.attr('data-price');
|
|---|
| 205 | priceInput.value = price || 0;
|
|---|
| 206 | calculateTotal();
|
|---|
| 207 | });
|
|---|
| 208 |
|
|---|
| 209 | quantityInput.addEventListener('input', calculateTotal);
|
|---|
| 210 | priceInput.addEventListener('input', calculateTotal);
|
|---|
| 211 |
|
|---|
| 212 | updateIndices();
|
|---|
| 213 | }
|
|---|
| 214 |
|
|---|
| 215 | function removeItem(button) {
|
|---|
| 216 |
|
|---|
| 217 | const row = button.closest('.item-row');
|
|---|
| 218 | $(row).find('.product-select').select2('destroy');
|
|---|
| 219 |
|
|---|
| 220 | row.remove();
|
|---|
| 221 | updateIndices();
|
|---|
| 222 | calculateTotal();
|
|---|
| 223 | }
|
|---|
| 224 |
|
|---|
| 225 | function updateIndices() {
|
|---|
| 226 | document.querySelectorAll('#itemsContainer .item-row').forEach((row, index) => {
|
|---|
| 227 | const productSelect = row.querySelector('.product-select');
|
|---|
| 228 | const quantityInput = row.querySelector('.quantity-input');
|
|---|
| 229 | const priceInput = row.querySelector('.price-input');
|
|---|
| 230 |
|
|---|
| 231 | productSelect.name = `Items[${index}].ProductId`;
|
|---|
| 232 | quantityInput.name = `Items[${index}].Quantity`;
|
|---|
| 233 | priceInput.name = `Items[${index}].UnitPrice`;
|
|---|
| 234 | });
|
|---|
| 235 | }
|
|---|
| 236 |
|
|---|
| 237 | function calculateTotal() {
|
|---|
| 238 | let total = 0;
|
|---|
| 239 | document.querySelectorAll('#itemsContainer .item-row').forEach(row => {
|
|---|
| 240 | const quantity = parseFloat(row.querySelector('.quantity-input').value) || 0;
|
|---|
| 241 | const price = parseFloat(row.querySelector('.price-input').value) || 0;
|
|---|
| 242 | total += quantity * price;
|
|---|
| 243 | });
|
|---|
| 244 | document.getElementById('totalAmount').textContent = total.toFixed(2) + ' MKD';
|
|---|
| 245 | }
|
|---|
| 246 |
|
|---|
| 247 | document.getElementById('saleForm').addEventListener('submit', function(e) {
|
|---|
| 248 | const itemsCount = document.querySelectorAll('#itemsContainer .item-row').length;
|
|---|
| 249 |
|
|---|
| 250 | if (itemsCount === 0) {
|
|---|
| 251 | e.preventDefault();
|
|---|
| 252 | alert('You must add at least one product!');
|
|---|
| 253 | return false;
|
|---|
| 254 | }
|
|---|
| 255 |
|
|---|
| 256 | let allValid = true;
|
|---|
| 257 | document.querySelectorAll('#itemsContainer .item-row').forEach(row => {
|
|---|
| 258 | const productSelect = row.querySelector('.product-select');
|
|---|
| 259 | if (!productSelect.value) {
|
|---|
| 260 | allValid = false;
|
|---|
| 261 |
|
|---|
| 262 | productSelect.classList.add('is-invalid');
|
|---|
| 263 |
|
|---|
| 264 | $(row).find('.select2-selection').addClass('is-invalid-border');
|
|---|
| 265 | } else {
|
|---|
| 266 | productSelect.classList.remove('is-invalid');
|
|---|
| 267 | }
|
|---|
| 268 | });
|
|---|
| 269 |
|
|---|
| 270 | if (!allValid) {
|
|---|
| 271 | e.preventDefault();
|
|---|
| 272 | alert('Please select products for all items!');
|
|---|
| 273 | return false;
|
|---|
| 274 | }
|
|---|
| 275 |
|
|---|
| 276 | return true;
|
|---|
| 277 | });
|
|---|
| 278 | </script>
|
|---|
| 279 | }
|
|---|