Merge branch 'main' of git.golsystems.mx:juan.zapata/NETBien.backend

This commit is contained in:
Juan Felipe Zapata Moreno 2025-11-19 19:25:36 -06:00
commit 831d8312b6
8 changed files with 213 additions and 153 deletions

View File

@ -0,0 +1,114 @@
<?php
namespace App\Exports;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Concerns\WithTitle;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use App\Models\SaleItem;
use Illuminate\Support\Facades\DB;
class CashCloseReportExport implements FromCollection, WithHeadings, WithStyles, WithTitle
{
protected $cashCloses;
protected $cashCloseIds;
public function __construct($cashCloses)
{
$this->cashCloses = $cashCloses;
$this->cashCloseIds = $cashCloses->pluck('id')->toArray();
}
public function collection()
{
$data = collect();
// RESUMEN FINANCIERO
$totalIncome = $this->cashCloses->sum('income');
$totalExit = $this->cashCloses->sum('exit');
$totalCash = $this->cashCloses->sum('income_cash');
$totalCard = $this->cashCloses->sum('income_card');
$totalTransfer = $this->cashCloses->sum('income_transfer');
$balanceFinal = $this->cashCloses->sum('initial_balance') + $totalIncome - $totalExit;
$data->push(['RESUMEN FINANCIERO', '', '', '', '', '']);
$data->push(['Periodo Inicio', $this->cashCloses->last()?->opened_at, '', '', '', '']);
$data->push(['Periodo Fin', $this->cashCloses->first()?->closed_at, '', '', '', '']);
$data->push(['Total Ventas', $totalIncome, '', '', '', '']);
$data->push(['Efectivo', $totalCash, '', '', '', '']);
$data->push(['Tarjeta', $totalCard, '', '', '', '']);
$data->push(['Transferencia', $totalTransfer, '', '', '', '']);
$data->push(['Egresos', $totalExit, '', '', '', '']);
$data->push(['Balance Final', $balanceFinal, '', '', '', '']);
$data->push(['', '', '', '', '', '']); // Espacios
// ESTADÍSTICAS POR PAQUETE
$packageStats = DB::table('sale_items')
->join('sales', 'sale_items.sale_id', '=', 'sales.id')
->join('packages', 'sale_items.package_id', '=', 'packages.id')
->whereIn('sales.cash_close_id', $this->cashCloseIds)
->select(
'packages.name as paquete',
DB::raw('COUNT(*) as total_vendidos'),
DB::raw('SUM(packages.price) as total_ingresos')
)
->groupBy('packages.id', 'packages.name')
->get();
$data->push(['VENTAS POR PAQUETE', '', '', '', '', '']);
$data->push(['Paquete', 'Total Vendidos', 'Total Ingresos', '', '', '']);
foreach ($packageStats as $stat) {
$data->push([$stat->paquete, $stat->total_vendidos, $stat->total_ingresos, '', '', '']);
}
$data->push(['', '', '', '', '', '']); // Espacios
// VENTAS DETALLADAS
$detailedSales = SaleItem::whereHas('sale', function ($query) {
$query->whereIn('cash_close_id', $this->cashCloseIds);
})
->with([
'sale.client:id,name,paternal,maternal',
'sale:id,client_id,payment_method',
'simCard:id,iccid,msisdn',
'package:id,name,price'
])
->orderBy('id', 'asc')
->get();
$data->push(['VENTAS DETALLADAS', '', '', '', '', '']);
$data->push(['Nombre Comprador', 'ID SIM', 'Número Asignado', 'Paquete', 'Costo', 'Medio de Pago']);
foreach ($detailedSales as $item) {
$data->push([
$item->sale->client->full_name,
$item->simCard->iccid,
$item->simCard->msisdn,
$item->package->name,
$item->package->price,
ucfirst($item->sale->payment_method)
]);
}
return $data;
}
public function headings(): array
{
return [];
}
public function title(): string
{
return 'Reporte Corte de Caja';
}
public function styles(Worksheet $sheet)
{
return [
1 => ['font' => ['bold' => true, 'size' => 14]],
11 => ['font' => ['bold' => true, 'size' => 14]],
];
}
}

View File

@ -9,7 +9,8 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Notsoweb\ApiResponse\Enums\ApiResponse; use Notsoweb\ApiResponse\Enums\ApiResponse;
use phpseclib3\Crypt\RC2; use App\Exports\CashCloseReportExport;
use Maatwebsite\Excel\Facades\Excel;
/** /**
* *
@ -210,11 +211,11 @@ public function exportReport(Request $request)
$query = CashClose::with('user:id,name')->withCount('sales'); $query = CashClose::with('user:id,name')->withCount('sales');
if ($request->has('start_date') && $request->has('end_date')) { if ($request->has('start_date') && $request->has('end_date')) {
$query->whereBetween('close_at', [$request->start_date, $request->end_date]); $query->whereBetween('closed_at', [$request->start_date, $request->end_date]);
} elseif ($request->has('start_date')) { } elseif ($request->has('start_date')) {
$query->whereDate('close_at', '>=', $request->start_date); $query->whereDate('closed_at', '>=', $request->start_date);
} elseif ($request->has('end_date')) { } elseif ($request->has('end_date')) {
$query->whereDate('close_at', '<=', $request->end_date); $query->whereDate('closed_at', '<=', $request->end_date);
} else { } else {
$query->closed()->orderBy('id', 'desc')->limit(1); $query->closed()->orderBy('id', 'desc')->limit(1);
} }
@ -227,70 +228,11 @@ public function exportReport(Request $request)
]); ]);
} }
$cashCloseIds = $cashCloses->pluck('id')->toArray(); $filename = 'reporte_corte_caja_' . date('Y-m-d_His') . '.xlsx';
// Obtener ventas detalladas return Excel::download(
$detailedSales = SaleItem::whereHas('sale', function ($query) use ($cashCloseIds) { new CashCloseReportExport($cashCloses),
$query->whereIn('cash_close_id', $cashCloseIds); $filename
}) );
->with([
'sale.client:id,name,paternal,maternal',
'sale:id,client_id,payment_method',
'simCard:id,iccid,msisdn',
'package:id,name,price'
])
->orderBy('id', 'asc')
->get();
// Calcular totales
$totalIncome = $cashCloses->sum('income');
$totalExit = $cashCloses->sum('exit');
$totalCash = $cashCloses->sum('income_cash');
$totalCard = $cashCloses->sum('income_card');
$totalTransfer = $cashCloses->sum('income_transfer');
// Crear el CSV
$filename = 'reporte_corte_caja_' . date('Y-m-d_His') . '.csv';
$headers = [
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
];
$callback = function () use ($cashCloses, $detailedSales, $totalIncome, $totalExit, $totalCash, $totalCard, $totalTransfer) {
$file = fopen('php://output', 'w');
fprintf($file, chr(0xEF) . chr(0xBB) . chr(0xBF));
// RESUMEN FINANCIERO
fputcsv($file, ['RESUMEN FINANCIERO', '']);
fputcsv($file, ['Periodo Inicio', $cashCloses->last()?->opened_at]);
fputcsv($file, ['Periodo Fin', $cashCloses->first()?->closed_at]);
fputcsv($file, ['Total Ventas', number_format($totalIncome, 2)]);
fputcsv($file, ['Efectivo', number_format($totalCash, 2)]);
fputcsv($file, ['Tarjeta', number_format($totalCard, 2)]);
fputcsv($file, ['Transferencia', number_format($totalTransfer, 2)]);
fputcsv($file, ['Egresos', number_format($totalExit, 2)]);
fputcsv($file, []);
// VENTAS DETALLADAS
fputcsv($file, ['VENTAS DETALLADAS']);
fputcsv($file, ['Nombre Comprador', 'ID SIM', 'Número Asignado', 'Paquete', 'Costo', 'Medio de Pago']);
foreach ($detailedSales as $item) {
fputcsv($file, [
$item->sale->client->full_name,
"'" . $item->simCard->iccid . "'",
"'" . $item->simCard->msisdn . "'",
$item->package->name,
number_format($item->package->price, 2),
ucfirst($item->sale->payment_method)
]);
}
fclose($file);
};
return response()->stream($callback, 200, $headers);
} }
} }

View File

@ -126,69 +126,87 @@ public function import(Request $request)
$sheet = $spreadsheet->getActiveSheet(); $sheet = $spreadsheet->getActiveSheet();
$rows = $sheet->toArray(); $rows = $sheet->toArray();
foreach ($rows as $index => $row) { DB::beginTransaction();
if ($index === 0) continue;
$this->processRow([ try {
'iccid' => $row[4] ?? null, foreach ($rows as $index => $row) {
'msisdn' => $row[5] ?? null, if ($index === 0) continue;
'estado_de_la_sim' => $row[9] ?? null,
'usuario' => $row[8] ?? null, try {
$this->processRow([
'iccid' => $row[4] ?? null,
'msisdn' => $row[5] ?? null,
'estado_de_la_sim' => $row[9] ?? null,
'usuario' => $row[8] ?? null,
], $index + 1);
} catch (\Exception $e) {
// Capturar información detallada del error antes de revertir
DB::rollBack();
return ApiResponse::BAD_REQUEST->response([
'success' => false,
'message' => 'Error en la importación',
'error' => $e->getMessage(),
'fila' => $index + 1,
'datos_fila' => [
'iccid' => $row[4] ?? null,
'msisdn' => $row[5] ?? null,
'estado_de_la_sim' => $row[9] ?? null,
'usuario' => $row[8] ?? null,
],
'stats' => $this->stats,
], 500);
}
}
DB::commit();
return ApiResponse::OK->response([
'success' => true,
'message' => 'Importación completada',
'stats' => $this->stats,
'packages_created' => array_values(array_map(fn($p) => [
'name' => $p->name,
'price' => $p->price
], $this->packageCache)),
]); ]);
} catch (\Exception $e) {
DB::rollBack();
throw $e;
} }
return ApiResponse::OK->response([
'success' => true,
'message' => 'Importación completada',
'stats' => $this->stats,
'packages_created' => array_values(array_map(fn($p) => [
'name' => $p->name,
'price' => $p->price
], $this->packageCache)),
]);
} catch (\Exception $e) { } catch (\Exception $e) {
return ApiResponse::BAD_REQUEST->response([ return ApiResponse::BAD_REQUEST->response([
'success' => false, 'success' => false,
'message' => 'Error en la importación', 'message' => 'Error en la importación',
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'line' => $e->getLine(),
], 500); ], 500);
} }
} }
private function processRow(array $row) private function processRow(array $row, int $rowNumber = 0)
{ {
// Validar campos requeridos // Validar campos requeridos
if (empty($row['iccid']) || empty($row['msisdn'])) { if (empty($row['iccid']) || empty($row['msisdn'])) {
return; return;
} }
try { // Buscar o crear la SIM
DB::transaction(function () use ($row) { $sim = SimCard::where('iccid', $row['iccid'])->first();
// Buscar o crear la SIM
$sim = SimCard::where('iccid', $row['iccid'])->first();
if (!$sim) { if (!$sim) {
// No existe, crearla // No existe, crearla
$sim = SimCard::create([ $sim = SimCard::create([
'iccid' => $row['iccid'], 'iccid' => $row['iccid'],
'msisdn' => $row['msisdn'], 'msisdn' => $row['msisdn'],
'status' => SimCardStatus::AVAILABLE, 'status' => SimCardStatus::AVAILABLE,
]); ]);
$this->stats['created']++; $this->stats['created']++;
}
$this->processPackageFromText($sim, $row);
// Asignar cliente
$this->assignToClient($sim, $row);
});
} catch (\Exception $e) {
$this->stats['errors'][] = [
'iccid' => $row['iccid'] ?? 'N/A',
'error' => $e->getMessage(),
];
} }
$this->processPackageFromText($sim, $row);
// Asignar cliente
$this->assignToClient($sim, $row);
} }
private function processPackageFromText(SimCard $sim, array $row) private function processPackageFromText(SimCard $sim, array $row)
@ -250,10 +268,12 @@ private function getOrCreatePackage(string $type, float $price): Packages
return $this->packageCache[$cacheKey]; return $this->packageCache[$cacheKey];
} }
$package = Packages::firstOrCreate( $package = Packages::create([
['name' => $type, 'price' => $price], 'name' => $type,
['period' => 0, 'data_limit' => 0] 'price' => (float) $price,
); 'period' => 0,
'data_limit' => 0,
]);
if ($package->wasRecentlyCreated) { if ($package->wasRecentlyCreated) {
$this->stats['packages_created']++; $this->stats['packages_created']++;
@ -281,22 +301,14 @@ private function assignToClient(SimCard $sim, array $row)
if (!$client) { if (!$client) {
$nameParts = $this->splitFullName($usuario); $nameParts = $this->splitFullName($usuario);
try { $client = Client::create([
$client = Client::create([ 'full_name' => $usuario,
'full_name' => $usuario, 'name' => $nameParts['name'],
'name' => $nameParts['name'], 'paternal' => $nameParts['paternal'],
'paternal' => $nameParts['paternal'], 'maternal' => $nameParts['maternal'],
'maternal' => $nameParts['maternal'], ]);
]);
$this->stats['clients_created']++; $this->stats['clients_created']++;
} catch (\Exception $e) {
$this->stats['errors'][] = [
'usuario' => $usuario,
'error' => 'Error al crear cliente: ' . $e->getMessage()
];
return;
}
} }
$existingRelation = ClientSim::where('client_id', $client->id) $existingRelation = ClientSim::where('client_id', $client->id)
@ -308,24 +320,16 @@ private function assignToClient(SimCard $sim, array $row)
return; return;
} }
try { ClientSim::create([
ClientSim::create([ 'client_id' => $client->id,
'client_id' => $client->id, 'sim_card_id' => $sim->id,
'sim_card_id' => $sim->id, 'assigned_at' => now(),
'assigned_at' => now(), 'is_active' => true,
'is_active' => true, ]);
]);
$sim->update(['status' => SimCardStatus::ASSIGNED]); $sim->update(['status' => SimCardStatus::ASSIGNED]);
$this->stats['assigned']++; $this->stats['assigned']++;
} catch (\Exception $e) {
$this->stats['errors'][] = [
'iccid' => $sim->iccid,
'usuario' => $usuario,
'error' => 'Error al asignar cliente: ' . $e->getMessage()
];
}
} }
private function splitFullName(string $fullName): array private function splitFullName(string $fullName): array

View File

@ -32,7 +32,7 @@ public function rules(): array
'maternal' => ['required', 'string'], 'maternal' => ['required', 'string'],
'email' => ['nullable', 'email'], 'email' => ['nullable', 'email'],
'phone' => ['nullable', 'string', 'max:10'], 'phone' => ['nullable', 'string', 'max:10'],
'rfc' => ['required', 'string', 'max:13'], 'rfc' => ['nullable', 'string', 'max:13'],
]; ];
} }

View File

@ -32,7 +32,7 @@ public function rules(): array
'maternal' => ['sometimes', 'string', 'max:100'], 'maternal' => ['sometimes', 'string', 'max:100'],
'email' => ['sometimes', 'email'], 'email' => ['sometimes', 'email'],
'phone' => ['nullable', 'string', 'max:20'], 'phone' => ['nullable', 'string', 'max:20'],
'rfc' => ['sometimes', 'string', 'max:13'], 'rfc' => ['nullable', 'string', 'max:13'],
]; ];
} }

View File

@ -26,9 +26,9 @@ public function rules(): array
{ {
return [ return [
'name' => ['required', 'string', 'max:80'], 'name' => ['required', 'string', 'max:80'],
'price' => ['required', 'integer'], 'price' => ['required', 'numeric'],
'period' => ['required', 'integer'], 'period' => ['required', 'numeric'],
'data_limit' => ['required', 'integer'], 'data_limit' => ['required', 'numeric'],
]; ];
} }

View File

@ -28,7 +28,7 @@ public function rules(): array
{ {
return [ return [
'iccid' => ['required', 'string', 'max:25', 'unique:sim_cards,iccid'], 'iccid' => ['required', 'string', 'max:25', 'unique:sim_cards,iccid'],
'msisdn' => ['required', 'string', 'max:10', 'unique:sim_cards,msisdn'], 'msisdn' => ['required', 'string', 'max:15', 'unique:sim_cards,msisdn'],
'package_id' => ['nullable', 'integer', 'exists:packages,id'], 'package_id' => ['nullable', 'integer', 'exists:packages,id'],
]; ];
} }
@ -43,7 +43,7 @@ public function messages() : array
'msisdn.required' => 'El campo MSISDN es obligatorio.', 'msisdn.required' => 'El campo MSISDN es obligatorio.',
'msisdn.string' => 'El campo MSISDN debe ser una cadena de texto.', 'msisdn.string' => 'El campo MSISDN debe ser una cadena de texto.',
'msisdn.max' => 'El campo MSISDN no debe exceder los 10 caracteres.', 'msisdn.max' => 'El campo MSISDN no debe exceder los 15 caracteres.',
'msisdn.unique' => 'El MSISDN ya está en uso.', 'msisdn.unique' => 'El MSISDN ya está en uso.',
'package_id.integer' => 'El paquete debe ser un número entero.', 'package_id.integer' => 'El paquete debe ser un número entero.',

View File

@ -27,7 +27,7 @@ public function authorize(): bool
public function rules(): array public function rules(): array
{ {
return [ return [
'msisdn' => ['required', 'string', 'max:10', 'unique:sim_cards,msisdn'], 'msisdn' => ['required', 'string', 'max:15', 'unique:sim_cards,msisdn'],
]; ];
} }