validate([ 'fecha_inicio' => 'required|date', 'fecha_fin' => 'required|date|after_or_equal:fecha_inicio', 'tier_id' => 'nullable|exists:client_tiers,id', ]); $fechaInicio = Carbon::parse($request->fecha_inicio)->startOfDay(); $fechaFin = Carbon::parse($request->fecha_fin)->endOfDay(); $tierId = $request->tier_id; // 2. OBTENER DATOS DE CLIENTES CON DESCUENTOS $clients = Client::whereNotNull('tier_id') ->with('tier:id,tier_name,discount_percentage') ->whereHas('sales', function($q) use ($fechaInicio, $fechaFin) { $q->whereBetween('created_at', [$fechaInicio, $fechaFin]); }) ->when($tierId, function($query) use ($tierId) { $query->where('tier_id', $tierId); }) ->get(); if ($clients->isEmpty()) { return response()->json(['message' => 'No se encontraron registros en el periodo especificado'], 404); } // 3. MAPEO DE DATOS $data = $clients->map(function($client) use ($fechaInicio, $fechaFin) { $sales = $client->sales() ->whereBetween('created_at', [$fechaInicio, $fechaFin]) ->get(); return [ 'numero' => $client->client_number, 'nombre' => $client->name, 'email' => $client->email ?? 'N/A', 'telefono' => $client->phone ?? 'N/A', 'tier' => $client->tier?->tier_name ?? 'N/A', 'descuento_porcentaje' => $client->tier?->discount_percentage ?? 0, 'total_compras' => $client->total_purchases, 'ventas_con_descuento' => $sales->count(), 'descuentos_recibidos' => $sales->sum('discount_amount'), 'promedio_descuento' => $sales->count() > 0 ? $sales->avg('discount_amount') : 0, ]; }); // 4. CONFIGURACIÓN EXCEL Y ESTILOS $fileName = 'Reporte_Descuentos_Clientes_' . $fechaInicio->format('Ymd') . '.xlsx'; $filePath = storage_path('app/temp/' . $fileName); if (!file_exists(dirname($filePath))) mkdir(dirname($filePath), 0755, true); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); // Fuente Global $sheet->getParent()->getDefaultStyle()->getFont()->setName('Arial'); $sheet->getParent()->getDefaultStyle()->getFont()->setSize(10); // Estilos Comunes $styleBox = [ 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => '000000']]], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER] ]; $styleLabel = [ 'font' => ['size' => 12, 'bold' => true], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_RIGHT, 'vertical' => Alignment::VERTICAL_CENTER] ]; $styleTableHeader = [ 'font' => ['bold' => true, 'size' => 10, 'color' => ['rgb' => 'FFFFFF']], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '4472C4']], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true], 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]] ]; // --- ESTRUCTURA DEL DOCUMENTO --- $sheet->getRowDimension(2)->setRowHeight(10); $sheet->getRowDimension(3)->setRowHeight(25); $sheet->getRowDimension(5)->setRowHeight(30); // --- TÍTULO PRINCIPAL --- $sheet->mergeCells('A3:J3'); $sheet->setCellValue('A3', 'REPORTE DE DESCUENTOS A CLIENTES'); $sheet->getStyle('A3')->applyFromArray([ 'font' => ['bold' => true, 'size' => 16, 'color' => ['rgb' => '000000']], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER], ]); // --- INFORMACIÓN DEL PERIODO --- Carbon::setLocale('es'); if ($fechaInicio->format('m/Y') === $fechaFin->format('m/Y')) { $periodoTexto = 'del ' . $fechaInicio->format('d') . ' al ' . $fechaFin->format('d') . ' de ' . $fechaFin->translatedFormat('F \d\e Y'); } else { $periodoTexto = 'del ' . $fechaInicio->format('d/m/Y') . ' al ' . $fechaFin->format('d/m/Y'); } $sheet->mergeCells('A5:B5'); $sheet->setCellValue('A5', 'PERÍODO:'); $sheet->getStyle('A5')->applyFromArray($styleLabel); $sheet->mergeCells('C5:J5'); $sheet->setCellValue('C5', $periodoTexto); $sheet->getStyle('C5:J5')->applyFromArray($styleBox); $sheet->getStyle('C5')->getFont()->setSize(12); // --- RESUMEN DE TOTALES --- $totalClientes = $data->count(); $totalVentas = $data->sum('ventas_con_descuento'); $totalDescuentos = $data->sum('descuentos_recibidos'); $row = 7; $sheet->setCellValue('A' . $row, 'TOTAL CLIENTES CON DESCUENTOS:'); $sheet->setCellValue('C' . $row, $totalClientes); $sheet->setCellValue('E' . $row, 'TOTAL VENTAS:'); $sheet->setCellValue('G' . $row, $totalVentas); $sheet->setCellValue('I' . $row, 'TOTAL DESCUENTOS:'); $sheet->setCellValue('J' . $row, '$' . number_format($totalDescuentos, 2)); $sheet->getStyle('A' . $row . ':J' . $row)->getFont()->setBold(true); $sheet->getStyle('C' . $row)->getFont()->setSize(12)->getColor()->setRGB('4472C4'); $sheet->getStyle('G' . $row)->getFont()->setSize(12)->getColor()->setRGB('4472C4'); $sheet->getStyle('J' . $row)->getFont()->setSize(12)->getColor()->setRGB('008000'); // --- ENCABEZADOS DE TABLA --- $h = 9; $headers = [ 'A' => 'No.', 'B' => "NÚMERO\nCLIENTE", 'C' => 'NOMBRE', 'D' => 'EMAIL', 'E' => 'TELÉFONO', 'F' => 'NIVEL', 'G' => "DESCUENTO\n(%)", 'H' => "VENTAS CON\nDESCUENTO", 'I' => "DESCUENTOS\nRECIBIDOS", 'J' => "PROMEDIO\nDESCUENTO" ]; foreach ($headers as $col => $text) { $sheet->setCellValue("{$col}{$h}", $text); } $sheet->getStyle("A{$h}:J{$h}")->applyFromArray($styleTableHeader); $sheet->getRowDimension($h)->setRowHeight(35); // --- LLENADO DE DATOS --- $row = 10; $i = 1; foreach ($data as $item) { $sheet->setCellValue('A' . $row, $i); $sheet->setCellValue('B' . $row, $item['numero']); $sheet->setCellValue('C' . $row, $item['nombre']); $sheet->setCellValue('D' . $row, $item['email']); $sheet->setCellValue('E' . $row, $item['telefono']); $sheet->setCellValue('F' . $row, $item['tier']); $sheet->setCellValue('G' . $row, $item['descuento_porcentaje'] . '%'); $sheet->setCellValue('H' . $row, $item['ventas_con_descuento']); $sheet->setCellValue('I' . $row, '$' . number_format($item['descuentos_recibidos'], 2)); $sheet->setCellValue('J' . $row, '$' . number_format($item['promedio_descuento'], 2)); // Estilos de fila $sheet->getStyle("A{$row}:J{$row}")->applyFromArray([ 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], 'font' => ['size' => 10] ]); // Centrados $sheet->getStyle("A{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $sheet->getStyle("B{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $sheet->getStyle("G{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $sheet->getStyle("H{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); // Color alterno de filas if ($i % 2 == 0) { $sheet->getStyle("A{$row}:J{$row}")->getFill() ->setFillType(Fill::FILL_SOLID) ->getStartColor()->setRGB('F2F2F2'); } $row++; $i++; } // --- ANCHOS DE COLUMNA --- $sheet->getColumnDimension('A')->setWidth(5); $sheet->getColumnDimension('B')->setWidth(15); $sheet->getColumnDimension('C')->setWidth(25); $sheet->getColumnDimension('D')->setWidth(25); $sheet->getColumnDimension('E')->setWidth(15); $sheet->getColumnDimension('F')->setWidth(12); $sheet->getColumnDimension('G')->setWidth(12); $sheet->getColumnDimension('H')->setWidth(15); $sheet->getColumnDimension('I')->setWidth(18); $sheet->getColumnDimension('J')->setWidth(18); $writer = new Xlsx($spreadsheet); $writer->save($filePath); return response()->download($filePath, $fileName)->deleteFileAfterSend(true); } /** * Generar reporte Excel de ventas */ public function salesReport(Request $request) { // 1. VALIDACIÓN $request->validate([ 'fecha_inicio' => 'required|date', 'fecha_fin' => 'required|date|after_or_equal:fecha_inicio', 'client_id' => 'nullable|exists:clients,id', 'user_id' => 'nullable|exists:users,id', 'status' => 'nullable|in:completed,cancelled', ]); $fechaInicio = Carbon::parse($request->fecha_inicio)->startOfDay(); $fechaFin = Carbon::parse($request->fecha_fin)->endOfDay(); // 2. CONSULTA DE VENTAS $sales = Sale::with(['client:id,name,client_number,rfc', 'user:id,name', 'details']) ->whereBetween('created_at', [$fechaInicio, $fechaFin]) ->when($request->client_id, function($query) use ($request) { $query->where('client_id', $request->client_id); }) ->when($request->user_id, function($query) use ($request) { $query->where('user_id', $request->user_id); }) ->when($request->status, function($query) use ($request) { $query->where('status', $request->status); }) ->orderBy('created_at', 'desc') ->get(); if ($sales->isEmpty()) { return response()->json(['message' => 'No se encontraron ventas en el periodo especificado'], 404); } // 3. MAPEO DE DATOS $data = $sales->map(function($sale) { return [ 'folio' => $sale->invoice_number, 'fecha' => $sale->created_at->format('d/m/Y H:i'), 'cliente' => $sale->client?->name ?? 'N/A', 'rfc_cliente' => $sale->client?->rfc ?? 'N/A', 'vendedor' => $sale->user?->name ?? 'N/A', 'subtotal' => (float) $sale->subtotal, 'iva' => (float) $sale->tax, 'descuento' => (float) ($sale->discount_amount ?? 0), 'total' => (float) $sale->total, 'metodo_pago' => $this->translatePaymentMethod($sale->payment_method), 'status' => $sale->status === 'completed' ? 'Completada' : 'Cancelada', 'productos' => $sale->details->count(), ]; }); // 4. CONFIGURACIÓN EXCEL $fileName = 'Reporte_Ventas_' . $fechaInicio->format('Ymd') . '_' . $fechaFin->format('Ymd') . '.xlsx'; $filePath = storage_path('app/temp/' . $fileName); if (!file_exists(dirname($filePath))) mkdir(dirname($filePath), 0755, true); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); // Fuente Global $sheet->getParent()->getDefaultStyle()->getFont()->setName('Arial'); $sheet->getParent()->getDefaultStyle()->getFont()->setSize(10); // Estilos Comunes $styleBox = [ 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => '000000']]], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER] ]; $styleLabel = [ 'font' => ['size' => 12, 'bold' => true], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_RIGHT, 'vertical' => Alignment::VERTICAL_CENTER] ]; $styleTableHeader = [ 'font' => ['bold' => true, 'size' => 10, 'color' => ['rgb' => 'FFFFFF']], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '2E75B6']], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true], 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]] ]; // --- ESTRUCTURA DEL DOCUMENTO --- $lastCol = 'L'; $sheet->getRowDimension(2)->setRowHeight(10); $sheet->getRowDimension(3)->setRowHeight(25); $sheet->getRowDimension(5)->setRowHeight(30); // --- TÍTULO PRINCIPAL --- $sheet->mergeCells("A3:{$lastCol}3"); $sheet->setCellValue('A3', 'REPORTE DE VENTAS'); $sheet->getStyle('A3')->applyFromArray([ 'font' => ['bold' => true, 'size' => 16, 'color' => ['rgb' => '000000']], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER], ]); // --- INFORMACIÓN DEL PERIODO --- Carbon::setLocale('es'); if ($fechaInicio->format('m/Y') === $fechaFin->format('m/Y')) { $periodoTexto = 'del ' . $fechaInicio->format('d') . ' al ' . $fechaFin->format('d') . ' de ' . $fechaFin->translatedFormat('F \d\e Y'); } else { $periodoTexto = 'del ' . $fechaInicio->format('d/m/Y') . ' al ' . $fechaFin->format('d/m/Y'); } $sheet->mergeCells('A5:B5'); $sheet->setCellValue('A5', 'PERÍODO:'); $sheet->getStyle('A5')->applyFromArray($styleLabel); $sheet->mergeCells("C5:{$lastCol}5"); $sheet->setCellValue('C5', $periodoTexto); $sheet->getStyle("C5:{$lastCol}5")->applyFromArray($styleBox); $sheet->getStyle('C5')->getFont()->setSize(12); // --- RESUMEN DE TOTALES --- $totalVentas = $data->count(); $totalSubtotal = $data->sum('subtotal'); $totalIva = $data->sum('iva'); $totalDescuentos = $data->sum('descuento'); $totalMonto = $data->sum('total'); $row = 7; $sheet->setCellValue('A' . $row, 'TOTAL VENTAS:'); $sheet->setCellValue('B' . $row, $totalVentas); $sheet->setCellValue('D' . $row, 'SUBTOTAL:'); $sheet->setCellValue('E' . $row, '$' . number_format($totalSubtotal, 2)); $sheet->setCellValue('G' . $row, 'IVA:'); $sheet->setCellValue('H' . $row, '$' . number_format($totalIva, 2)); $sheet->setCellValue('I' . $row, 'DESCUENTOS:'); $sheet->setCellValue('J' . $row, '$' . number_format($totalDescuentos, 2)); $sheet->setCellValue('K' . $row, 'TOTAL:'); $sheet->setCellValue('L' . $row, '$' . number_format($totalMonto, 2)); $sheet->getStyle("A{$row}:{$lastCol}{$row}")->getFont()->setBold(true); $sheet->getStyle('B' . $row)->getFont()->setSize(12)->getColor()->setRGB('2E75B6'); $sheet->getStyle('E' . $row)->getFont()->setSize(12)->getColor()->setRGB('2E75B6'); $sheet->getStyle('H' . $row)->getFont()->setSize(12)->getColor()->setRGB('2E75B6'); $sheet->getStyle('J' . $row)->getFont()->setSize(12)->getColor()->setRGB('FF6600'); $sheet->getStyle('L' . $row)->getFont()->setSize(12)->getColor()->setRGB('008000'); // --- ENCABEZADOS DE TABLA --- $h = 9; $headers = [ 'A' => 'No.', 'B' => 'FOLIO', 'C' => 'FECHA', 'D' => 'CLIENTE', 'E' => 'RFC', 'F' => 'VENDEDOR', 'G' => 'SUBTOTAL', 'H' => 'IVA', 'I' => 'DESCUENTO', 'J' => 'TOTAL', 'K' => "MÉTODO\nPAGO", 'L' => 'ESTADO', ]; foreach ($headers as $col => $text) { $sheet->setCellValue("{$col}{$h}", $text); } $sheet->getStyle("A{$h}:{$lastCol}{$h}")->applyFromArray($styleTableHeader); $sheet->getRowDimension($h)->setRowHeight(35); // --- LLENADO DE DATOS --- $row = 10; $i = 1; foreach ($data as $item) { $sheet->setCellValue('A' . $row, $i); $sheet->setCellValue('B' . $row, $item['folio']); $sheet->setCellValue('C' . $row, $item['fecha']); $sheet->setCellValue('D' . $row, $item['cliente']); $sheet->setCellValue('E' . $row, $item['rfc_cliente']); $sheet->setCellValue('F' . $row, $item['vendedor']); $sheet->setCellValue('G' . $row, '$' . number_format($item['subtotal'], 2)); $sheet->setCellValue('H' . $row, '$' . number_format($item['iva'], 2)); $sheet->setCellValue('I' . $row, '$' . number_format($item['descuento'], 2)); $sheet->setCellValue('J' . $row, '$' . number_format($item['total'], 2)); $sheet->setCellValue('K' . $row, $item['metodo_pago']); $sheet->setCellValue('L' . $row, $item['status']); // Estilos de fila $sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([ 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], 'font' => ['size' => 10] ]); // Centrados $sheet->getStyle("A{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $sheet->getStyle("B{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $sheet->getStyle("C{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $sheet->getStyle("K{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $sheet->getStyle("L{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); // Color de estado if ($item['status'] === 'Cancelada') { $sheet->getStyle("L{$row}")->getFont()->getColor()->setRGB('FF0000'); } // Color alterno de filas if ($i % 2 == 0) { $sheet->getStyle("A{$row}:{$lastCol}{$row}")->getFill() ->setFillType(Fill::FILL_SOLID) ->getStartColor()->setRGB('F2F2F2'); } $row++; $i++; } // --- ANCHOS DE COLUMNA --- $sheet->getColumnDimension('A')->setWidth(5); $sheet->getColumnDimension('B')->setWidth(22); $sheet->getColumnDimension('C')->setWidth(18); $sheet->getColumnDimension('D')->setWidth(22); $sheet->getColumnDimension('E')->setWidth(16); $sheet->getColumnDimension('F')->setWidth(20); $sheet->getColumnDimension('G')->setWidth(14); $sheet->getColumnDimension('H')->setWidth(12); $sheet->getColumnDimension('I')->setWidth(14); $sheet->getColumnDimension('J')->setWidth(14); $sheet->getColumnDimension('K')->setWidth(14); $sheet->getColumnDimension('L')->setWidth(14); $writer = new Xlsx($spreadsheet); $writer->save($filePath); return response()->download($filePath, $fileName)->deleteFileAfterSend(true); } /** * Traducir método de pago */ private function translatePaymentMethod(?string $method): string { return match($method) { 'cash' => 'Efectivo', 'credit_card' => 'T. Crédito', 'debit_card' => 'T. Débito', 'transfer' => 'Transferencia', default => $method ?? 'N/A', }; } }