1materialAdmin 2 3 //================================================= 4 // ITEMS SALES REPORT 5 //================================================= 6 .controller('itemsSalesReportCtrl', function ($scope, growlService, Item, Utils, Session) { 7 var _this = this; 8 9 $scope.primaryButtonHandler = (addonName) => Utils.requestAddon(addonName); 10 const user = Session.get(); 11 $scope.isSalesReportEnabled = user?.company?.addons?.includes("stock"); 12 13 // Initialize variables 14 _this.salesData = []; 15 _this.loading = false; 16 _this.searched = false; 17 _this.error = null; 18 _this.period = {}; 19 20 /*********** DATE **************/ 21 _this.dateOptions = { 22 startingDay: 1, 23 showWeeks: "false", 24 }; 25 26 _this.formats = ["dd/MM/yyyy"]; 27 _this.format = _this.formats[0]; 28 29 _this.status = {}; 30 31 _this.open = function ($event, opened) { 32 $event.preventDefault(); 33 $event.stopPropagation(); 34 _this.status[opened] = true; 35 }; 36 37 _this.filters = { 38 start_date: new Date(), 39 end_date: new Date() 40 }; 41 42 // Selection state for predefined date range buttons 43 _this.thisWeekSelected = false; 44 _this.thisMonthSelected = false; 45 _this.previousMonthSelected = false; 46 _this.thisYearSelected = false; 47 48 // Clear all selection states 49 _this.clearSelectionStates = function() { 50 _this.thisWeekSelected = false; 51 _this.thisMonthSelected = false; 52 _this.previousMonthSelected = false; 53 _this.thisYearSelected = false; 54 }; 55 56 // Date change handler 57 _this.dateChanged = function() { 58 _this.error = null; 59 if (_this.filters.start_date && _this.filters.end_date) { 60 var startDate = new Date(_this.filters.start_date); 61 var endDate = new Date(_this.filters.end_date); 62 63 if (startDate > endDate) { 64 _this.error = 'La fecha de inicio no puede ser mayor que la fecha de fin.'; 65 return; 66 } 67 68 // Check if date range is not too long (optional validation) 69 var diffTime = Math.abs(endDate - startDate); 70 var diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); 71 72 if (diffDays > 365) { 73 growlService.growl('El perÃodo seleccionado es muy largo. Se recomienda usar perÃodos menores a un año.', 'warning'); 74 } 75 _this.loadReport(); 76 } 77 }; 78 79 // Predefined date ranges 80 _this.setThisWeek = function() { 81 _this.clearSelectionStates(); 82 _this.thisWeekSelected = true; 83 84 var today = new Date(); 85 var firstDayOfWeek = new Date(today.setDate(today.getDate() - today.getDay() + 1)); // Monday 86 var lastDayOfWeek = new Date(today.setDate(today.getDate() - today.getDay() + 7)); // Sunday 87 88 _this.filters.start_date = firstDayOfWeek.toISOString().split('T')[0]; 89 _this.filters.end_date = lastDayOfWeek.toISOString().split('T')[0]; 90 _this.dateChanged(); 91 }; 92 93 _this.setThisMonth = function() { 94 _this.clearSelectionStates(); 95 _this.thisMonthSelected = true; 96 97 var today = new Date(); 98 var firstDay = new Date(today.getFullYear(), today.getMonth(), 1); 99 var lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0); 100 101 _this.filters.start_date = firstDay.toISOString().split('T')[0]; 102 _this.filters.end_date = lastDay.toISOString().split('T')[0]; 103 _this.dateChanged(); 104 }; 105 106 _this.setPreviousMonth = function() { 107 _this.clearSelectionStates(); 108 _this.previousMonthSelected = true; 109 110 var today = new Date(); 111 var firstDay = new Date(today.getFullYear(), today.getMonth() - 1, 1); 112 var lastDay = new Date(today.getFullYear(), today.getMonth(), 0); 113 114 _this.filters.start_date = firstDay.toISOString().split('T')[0]; 115 _this.filters.end_date = lastDay.toISOString().split('T')[0]; 116 _this.dateChanged(); 117 }; 118 119 _this.setThisYear = function() { 120 _this.clearSelectionStates(); 121 _this.thisYearSelected = true; 122 123 var today = new Date(); 124 var firstDay = new Date(today.getFullYear(), 0, 1); 125 var lastDay = new Date(today.getFullYear(), 11, 31); 126 127 _this.filters.start_date = firstDay.toISOString().split('T')[0]; 128 _this.filters.end_date = lastDay.toISOString().split('T')[0]; 129 _this.dateChanged(); 130 }; 131 132 // Load sales report 133 _this.loadReport = function() { 134 if (!$scope.isSalesReportEnabled) { 135 growlService.growlError('El complemento se encuentra deshabilitado'); 136 return; 137 } 138 139 if (!_this.filters.start_date || !_this.filters.end_date) { 140 growlService.growlError('Seleccione las fechas de inicio y fin.'); 141 return; 142 } 143 144 _this.loading = true; 145 _this.error = null; 146 _this.searched = true; 147 148 var params = { 149 start_date: _this.filters.start_date, 150 end_date: _this.filters.end_date 151 }; 152 153 Item.salesReport(params) 154 .then(function(response) { 155 _this.salesData = response.sales_report || []; 156 _this.period = response.period || {}; 157 }) 158 .catch(function(error) { 159 console.error('Error loading sales report:', error); 160 161 var errorMessage = 'Error al cargar el reporte de ventas.'; 162 if (error.data && error.data.error) { 163 errorMessage = error.data.error; 164 } else if (error.status === 400) { 165 errorMessage = 'Parámetros de fecha inválidos.'; 166 } else if (error.status === 401) { 167 errorMessage = 'No tiene permisos para acceder a este reporte.'; 168 } else if (error.status >= 500) { 169 errorMessage = 'Error interno del servidor. Intente nuevamente más tarde.'; 170 } 171 172 _this.error = errorMessage; 173 growlService.growlError(errorMessage); 174 }) 175 .finally(function() { 176 _this.loading = false;
177 }); 178 }; 179 180 // Calculate totals 181 _this.getTotalQuantity = function() { 182 return _this.salesData.reduce(function(sum, item) { 183 return sum + (parseFloat(item.total_quantity_sold) || 0); 184 }, 0); 185 }; 186 187 _this.getTotalAmount = function() { 188 return _this.salesData.reduce(function(sum, item) { 189 return sum + (parseFloat(item.total_amount) || 0); 190 }, 0); 191 }; 192 193 _this.getAveragePrice = function() { 194 var totalQuantity = _this.getTotalQuantity(); 195 var totalAmount = _this.getTotalAmount(); 196 197 if (totalQuantity === 0) return 0; 198 return totalAmount / totalQuantity; 199 }; 200 201 // Export to Excel 202 _this.exportToExcel = function() { 203 if (_this.salesData.length === 0) { 204 growlService.growlWarning('No hay datos para exportar.'); 205 return; 206 } 207 208 var createXLSLFormatObj = []; 209 210 // Title 211 createXLSLFormatObj.push(['Reporte de Ventas por ArtÃculo']); 212 213 // Period 214 var startDateFormatted = _this.period.start_date ? new Date(_this.period.start_date).toLocaleDateString('es-ES') : ''; 215 var endDateFormatted = _this.period.end_date ? new Date(_this.period.end_date).toLocaleDateString('es-ES') : ''; 216 createXLSLFormatObj.push(['DESDE', startDateFormatted, 'HASTA', endDateFormatted]); 217 218 // Empty row 219 createXLSLFormatObj.push([]); 220 221 // Headers 222 var xlsHeader = ['Tipo', 'ID ArtÃculo', 'Nombre del ArtÃculo', 'SKU', 'Cantidad Vendida', 'Monto Total']; 223 createXLSLFormatObj.push(xlsHeader); 224 225 // Data rows 226 _this.salesData.forEach(function(item) { 227 var row = [ 228 item.is_product ? 'Producto' : 'Servicio', 229 item.item_id, 230 item.item_name, 231 item.item_code || '-', 232 parseFloat(item.total_quantity_sold) || 0, 233 parseFloat(item.total_amount) || 0 234 ]; 235 createXLSLFormatObj.push(row); 236 }); 237 238 // Empty row 239 createXLSLFormatObj.push([]); 240 241 // Totals 242 createXLSLFormatObj.push(['TOTALES', '', '', '', _this.getTotalQuantity(), _this.getTotalAmount()]); 243 244 // Generate filename 245 var filename = 'reporte-ventas-items-' + startDateFormatted.replace(/\//g, '-') + '-a-' + endDateFormatted.replace(/\//g, '-') + '.xlsx'; 246 247 // Create workbook 248 var wb = XLSX.utils.book_new(); 249 var ws = XLSX.utils.aoa_to_sheet(createXLSLFormatObj); 250 251 // Add worksheet to workbook 252 XLSX.utils.book_append_sheet(wb, ws, 'Reporte de Ventas'); 253 254 // Download file 255 XLSX.writeFile(wb, filename); 256 257 growlService.growl('Reporte exportado exitosamente.', 'success'); 258 }; 259 260 _this.setThisWeek(); 261 262 });
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.