404 lines
15 KiB
PHP
404 lines
15 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Netbien;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\CashClose;
|
|
use App\Models\Sale;
|
|
use App\Models\SaleItem;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
|
use App\Exports\CashCloseReportExport;
|
|
use Maatwebsite\Excel\Facades\Excel;
|
|
|
|
/**
|
|
*
|
|
*/
|
|
class CashCloseController extends Controller
|
|
{
|
|
|
|
public function index(Request $request)
|
|
{
|
|
$query = CashClose::with('user:id,name')->withCount('sales');
|
|
|
|
if ($request->has('status')) {
|
|
$query->where('status', $request->status);
|
|
}
|
|
|
|
if ($request->has('date')) {
|
|
$query->whereDate('close_date', $request->date);
|
|
}
|
|
|
|
$cashCloses = $query->orderBy('id', 'asc')
|
|
->paginate(config('app.pagination'));
|
|
|
|
return ApiResponse::OK->response([
|
|
'cash_closes' => $cashCloses,
|
|
]);
|
|
}
|
|
|
|
public function report(Request $request)
|
|
{
|
|
$request->validate([
|
|
'start_date' => 'nullable|date',
|
|
'end_date' => 'nullable|date|after_or_equal:start_date',
|
|
]);
|
|
|
|
$query = CashClose::with('user:id,name')->withCount('sales');
|
|
|
|
if ($request->has('start_date') && $request->has('end_date')) {
|
|
$query->whereBetween('close_date', [$request->start_date, $request->end_date]);
|
|
} elseif ($request->has('start_date')) {
|
|
$query->whereDate('close_date', '>=', $request->start_date);
|
|
} elseif ($request->has('end_date')) {
|
|
$query->whereDate('close_date', '<=', $request->end_date);
|
|
} else {
|
|
$query->closed()->orderBy('id', 'desc')->limit(1);
|
|
}
|
|
|
|
// Información del corte de caja
|
|
$cashCloses = $query->orderBy('id', 'desc')->get();
|
|
|
|
if ($cashCloses->isEmpty()) {
|
|
return ApiResponse::NOT_FOUND->response([
|
|
'message' => 'No se encontraron cortes de caja en el rango de fechas especificado.',
|
|
]);
|
|
}
|
|
|
|
$cashCloseIds = $cashCloses->pluck('id')->toArray();
|
|
|
|
// Estadísticas por paquete (Total de Paquetes Vendidos por Tipo)
|
|
$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', $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();
|
|
|
|
// Estadísticas por duración (Total de Ventas por Duración)
|
|
$durationStats = 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', $cashCloseIds)
|
|
->select(
|
|
'packages.period as duracion_dias',
|
|
DB::raw('COUNT(DISTINCT sales.id) as total_ventas')
|
|
)
|
|
->groupBy('packages.period')
|
|
->orderBy('packages.period', 'asc')
|
|
->get();
|
|
|
|
// Reporte detallado de ventas
|
|
$detailedSales = SaleItem::whereHas('sale', function ($query) use ($cashCloseIds) {
|
|
$query->whereIn('cash_close_id', $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')
|
|
->paginate(config('app.pagination'))
|
|
->through(function ($item) {
|
|
return [
|
|
'nombre_comprador' => $item->sale->client->full_name,
|
|
'id_sim' => $item->simCard->iccid,
|
|
'numero_asignado' => $item->simCard->msisdn,
|
|
'paquete' => $item->package->name,
|
|
'costo' => $item->package->price,
|
|
'medio_pago' => $item->sale->payment_method
|
|
];
|
|
});
|
|
|
|
$totalIncome = $cashCloses->sum('income');
|
|
$totalExit = $cashCloses->sum('exit');
|
|
$totalCash = $cashCloses->sum('income_cash');
|
|
$totalCard = $cashCloses->sum('income_card');
|
|
$totalTransfer = $cashCloses->sum('income_transfer');
|
|
$balanceFinal = $cashCloses->sum('initial_balance') + $totalIncome - $totalExit;
|
|
|
|
|
|
return ApiResponse::OK->response([
|
|
'cash_closes' => $cashCloses,
|
|
'periodo' => [
|
|
'inicio' => $cashCloses->last()?->opened_at,
|
|
'fin' => $cashCloses->first()?->closed_at,
|
|
],
|
|
'resumen_financiero' => [
|
|
'total_ventas' => $totalIncome,
|
|
'efectivo' => $totalCash,
|
|
'tarjeta' => $totalCard,
|
|
'transferencia' => $totalTransfer,
|
|
'egresos' => $totalExit,
|
|
'balance_final' => $balanceFinal,
|
|
],
|
|
'ventas_paquete' => $packageStats,
|
|
'ventas_duracion' => $durationStats,
|
|
'ventas_detalladas' => $detailedSales,
|
|
]);
|
|
}
|
|
|
|
public function exportReport(Request $request)
|
|
{
|
|
$request->validate([
|
|
'start_date' => 'nullable|date',
|
|
'end_date' => 'nullable|date|after_or_equal:start_date',
|
|
]);
|
|
|
|
$query = CashClose::with('user:id,name')->withCount('sales');
|
|
|
|
if ($request->has('start_date') && $request->has('end_date')) {
|
|
$query->whereBetween('closed_at', [$request->start_date, $request->end_date]);
|
|
} elseif ($request->has('start_date')) {
|
|
$query->whereDate('closed_at', '>=', $request->start_date);
|
|
} elseif ($request->has('end_date')) {
|
|
$query->whereDate('closed_at', '<=', $request->end_date);
|
|
} else {
|
|
$query->closed()->orderBy('id', 'desc')->limit(1);
|
|
}
|
|
|
|
$cashCloses = $query->orderBy('id', 'desc')->get();
|
|
|
|
if ($cashCloses->isEmpty()) {
|
|
return ApiResponse::NOT_FOUND->response([
|
|
'message' => 'No se encontraron cortes de caja en el rango de fechas especificado.',
|
|
]);
|
|
}
|
|
|
|
$filename = 'reporte_corte_caja_' . date('Y-m-d_His') . '.xlsx';
|
|
|
|
return Excel::download(
|
|
new CashCloseReportExport($cashCloses),
|
|
$filename
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Preview de ventas disponibles para crear corte por rango de fechas
|
|
*/
|
|
public function previewSalesRange(Request $request)
|
|
{
|
|
$request->validate([
|
|
'start_date' => 'required|date',
|
|
'end_date' => 'required|date|after_or_equal:start_date',
|
|
]);
|
|
|
|
// Buscar ventas sin corte en el rango especificado
|
|
$availableSales = Sale::with([
|
|
'client:id,name,paternal,maternal',
|
|
'saleItems.simCard:id,iccid,msisdn',
|
|
'saleItems.package:id,name,price'
|
|
])
|
|
->whereNull('cash_close_id')
|
|
->whereBetween('sale_date', [$request->start_date, $request->end_date])
|
|
->orderBy('sale_date', 'asc')
|
|
->get();
|
|
|
|
if ($availableSales->isEmpty()) {
|
|
return ApiResponse::NOT_FOUND->response([
|
|
'message' => 'No se encontraron ventas disponibles en el rango de fechas especificado.',
|
|
'total_sales' => 0,
|
|
]);
|
|
}
|
|
|
|
// Calcular totales por método de pago
|
|
$paymentMethods = $availableSales->groupBy('payment_method')->map(function ($sales, $method) {
|
|
return [
|
|
'method' => $method,
|
|
'count' => $sales->count(),
|
|
'total' => $sales->sum('total_amount'),
|
|
];
|
|
})->values();
|
|
|
|
$totalAmount = $availableSales->sum('total_amount');
|
|
|
|
return ApiResponse::OK->response([
|
|
'message' => 'Ventas disponibles encontradas',
|
|
'periodo' => [
|
|
'start_date' => $request->start_date,
|
|
'end_date' => $request->end_date,
|
|
],
|
|
'resumen' => [
|
|
'total_ventas' => $availableSales->count(),
|
|
'monto_total' => $totalAmount,
|
|
'por_metodo_pago' => $paymentMethods,
|
|
],
|
|
'ventas' => $availableSales->map(function ($sale) {
|
|
return [
|
|
'id' => $sale->id,
|
|
'sale_date' => $sale->sale_date,
|
|
'client' => $sale->client->full_name ?? 'Sin cliente',
|
|
'payment_method' => $sale->payment_method,
|
|
'total_amount' => $sale->total_amount,
|
|
'items_count' => $sale->saleItems->count(),
|
|
];
|
|
}),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Crear corte de caja desde un rango de fechas
|
|
*/
|
|
public function createFromRange(Request $request)
|
|
{
|
|
$request->validate([
|
|
'start_date' => 'required|date',
|
|
'end_date' => 'required|date|after_or_equal:start_date',
|
|
'initial_balance' => 'sometimes|numeric|min:0',
|
|
'exit' => 'sometimes|numeric|min:0',
|
|
'notes' => 'sometimes|string|max:1000',
|
|
]);
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
// Buscar ventas sin corte en el rango especificado
|
|
$availableSales = Sale::whereNull('cash_close_id')
|
|
->whereBetween('sale_date', [$request->start_date, $request->end_date])
|
|
->get();
|
|
|
|
if ($availableSales->isEmpty()) {
|
|
return ApiResponse::NOT_FOUND->response([
|
|
'message' => 'No se encontraron ventas disponibles para crear el corte de caja.',
|
|
]);
|
|
}
|
|
|
|
// Verificar que ninguna venta ya esté en otro corte (doble verificación)
|
|
$salesWithClose = Sale::whereBetween('sale_date', [$request->start_date, $request->end_date])
|
|
->whereNotNull('cash_close_id')
|
|
->with('cashClose:id,status,opened_at,closed_at')
|
|
->get();
|
|
|
|
if ($salesWithClose->isNotEmpty()) {
|
|
return ApiResponse::BAD_REQUEST->response([
|
|
'message' => 'Algunas ventas en este rango ya están asignadas a otros cortes.',
|
|
'conflicting_sales' => $salesWithClose->map(fn($s) => [
|
|
'sale_id' => $s->id,
|
|
'sale_date' => $s->sale_date,
|
|
'cash_close_id' => $s->cash_close_id,
|
|
'cash_close_status' => $s->cashClose->status ?? null,
|
|
]),
|
|
], 422);
|
|
}
|
|
|
|
// Calcular totales
|
|
$totalAmount = $availableSales->sum('total_amount');
|
|
$paymentMethods = $availableSales->groupBy('payment_method');
|
|
|
|
$incomeCash = $paymentMethods->get('cash', collect())->sum('total_amount');
|
|
$incomeCard = $paymentMethods->get('card', collect())->sum('total_amount');
|
|
$incomeTransfer = $paymentMethods->get('transfer', collect())->sum('total_amount');
|
|
|
|
// Crear el corte de caja
|
|
$cashClose = CashClose::create([
|
|
'user_id' => auth()->id(),
|
|
'opened_at' => $request->start_date,
|
|
'closed_at' => $request->end_date,
|
|
'initial_balance' => $request->input('initial_balance', 0),
|
|
'income' => $totalAmount,
|
|
'exit' => $request->input('exit', 0),
|
|
'income_cash' => $incomeCash,
|
|
'income_card' => $incomeCard,
|
|
'income_transfer' => $incomeTransfer,
|
|
'status' => 'closed',
|
|
'type' => 'manual',
|
|
'notes' => $request->input('notes'),
|
|
]);
|
|
|
|
// Asignar las ventas al corte creado
|
|
Sale::whereIn('id', $availableSales->pluck('id'))
|
|
->update(['cash_close_id' => $cashClose->id]);
|
|
|
|
DB::commit();
|
|
|
|
$balanceFinal = $cashClose->initial_balance + $totalAmount - $cashClose->exit;
|
|
|
|
return ApiResponse::CREATED->response([
|
|
'message' => 'Corte de caja creado exitosamente',
|
|
'cash_close' => $cashClose->fresh('user'),
|
|
'resumen' => [
|
|
'periodo' => [
|
|
'inicio' => $cashClose->opened_at,
|
|
'fin' => $cashClose->closed_at,
|
|
],
|
|
'totales' => [
|
|
'fondo_inicial' => $cashClose->initial_balance,
|
|
'total_ventas' => $totalAmount,
|
|
'cantidad_ventas' => $availableSales->count(),
|
|
'efectivo' => $incomeCash,
|
|
'tarjeta' => $incomeCard,
|
|
'transferencia' => $incomeTransfer,
|
|
'egresos' => $cashClose->exit,
|
|
'balance_final' => $balanceFinal,
|
|
],
|
|
],
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
|
|
return ApiResponse::INTERNAL_ERROR->response([
|
|
'message' => 'Error al crear el corte de caja',
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deshacer (eliminar) un corte de caja
|
|
* Las ventas se liberan automáticamente (cash_close_id = null)
|
|
*/
|
|
public function undoCashClose(CashClose $cashClose)
|
|
{
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
// Verificar que el corte existe
|
|
if (!$cashClose) {
|
|
return ApiResponse::NOT_FOUND->response([
|
|
'message' => 'Corte de caja no encontrado.',
|
|
]);
|
|
}
|
|
|
|
// Obtener información antes de eliminar
|
|
$salesCount = $cashClose->sales()->count();
|
|
$cashCloseId = $cashClose->id;
|
|
$cashCloseType = $cashClose->type;
|
|
|
|
// Liberar las ventas (setear cash_close_id a null)
|
|
Sale::where('cash_close_id', $cashClose->id)
|
|
->update(['cash_close_id' => null]);
|
|
|
|
// Eliminar el corte (soft delete)
|
|
$cashClose->delete();
|
|
|
|
DB::commit();
|
|
|
|
return ApiResponse::OK->response([
|
|
'message' => 'Corte de caja eliminado exitosamente',
|
|
'info' => [
|
|
'cash_close_id' => $cashCloseId,
|
|
'type' => $cashCloseType,
|
|
'sales_liberated' => $salesCount,
|
|
'nota' => 'Las ventas han sido liberadas y están disponibles para ser asignadas a otro corte.',
|
|
],
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
|
|
return ApiResponse::INTERNAL_ERROR->response([
|
|
'message' => 'Error al eliminar el corte de caja',
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
}
|