Files
Verde-Web/app/Http/Controllers/Api/V1/Admin/AdminReportController.php
Developer 7f3357206f feat: implement fixed dashboard layout, custom geofencing shapes, and QC seeder
- UI: Refactored admin layout to a fixed 'App Shell' design with independent scrollbars for sidebar and content area.
- UI: Cleaned up sidebar partial structure to support nested scrolling.
- GEOFENCING: Added support for Circle and Rectangle tools in LGU configuration.
- GEOFENCING: Implemented high-precision (128-point) circle-to-polygon conversion for database storage.
- PAGINATION: Integrated standard Laravel pagination across Teams and Reports tables.
- PAGINATION: Added a global 'renderPagination' helper and 'Per Page' selector (25, 50, 100).
- DATA: Created QuezonCitySeeder with 142 official barangays and 3 months of historical collection/sales data.
- FIX: Corrected API total aggregations in reports to remain accurate during pagination.
- FIX: Resolved unique constraint and axis-order issues in spatial data seeders.
2026-07-03 11:19:08 +08:00

236 lines
9.7 KiB
PHP

<?php
namespace App\Http\Controllers\Api\V1\Admin;
use App\Http\Controllers\Api\V1\ApiController;
use App\Models\DailyCollectionStat;
use App\Models\DumpsiteRelease;
use App\Models\MonthlyStoreSale;
use App\Models\WeeklyRoutePerformance;
use App\Models\Tenant;
use App\Tenancy\Tenancy;
use App\Services\Report\Aggregator;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpFoundation\StreamedResponse;
class AdminReportController extends ApiController
{
public function __construct(private readonly Aggregator $aggregator) {}
public function dailyCollection(Request $request): JsonResponse
{
$data = $request->validate([
'from' => ['nullable', 'date'],
'to' => ['nullable', 'date'],
'barangay_id' => ['nullable', 'integer'],
'tenant_id' => ['nullable', 'integer'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:500'],
]);
$from = isset($data['from']) ? Carbon::parse($data['from'])->toDateString() : Carbon::now()->subDays(30)->toDateString();
$to = isset($data['to']) ? Carbon::parse($data['to'])->toDateString() : Carbon::today()->toDateString();
$perPage = (int) ($data['per_page'] ?? 50);
$tenant = $this->resolveTargetTenant($request);
return Tenancy::withTenant($tenant, function () use ($from, $to, $data, $perPage) {
$query = DailyCollectionStat::query()
->whereBetween('date', [$from, $to])
->when($data['barangay_id'] ?? null, fn ($q, $id) => $q->where('barangay_id', $id));
$totals = [
'total_scans' => (int) $query->sum('total_scans'),
'total_weight_kg' => (int) $query->sum('total_weight_kg'),
'unique_households' => (int) $query->sum('unique_households'),
];
$rows = $query->orderBy('date', 'desc')->paginate($perPage);
return $this->ok(
$rows->map(fn ($r) => [
'date' => $r->date->toDateString(),
'barangay_id' => $r->barangay_id,
'total_scans' => $r->total_scans,
'total_weight_kg' => $r->total_weight_kg,
'unique_households' => $r->unique_households,
'missed_pickups' => $r->missed_pickups,
]),
null,
array_merge($totals, [
'page' => $rows->currentPage(),
'per_page' => $rows->perPage(),
'total' => $rows->total(),
'last_page' => $rows->lastPage(),
'from' => $from,
'to' => $to,
])
);
});
}
public function tripPerformance(Request $request): JsonResponse
{
$data = $request->validate([
'from' => ['nullable', 'date'],
'to' => ['nullable', 'date'],
'route_id' => ['nullable', 'integer'],
'tenant_id' => ['nullable', 'integer'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:500'],
]);
$from = isset($data['from']) ? Carbon::parse($data['from'])->toDateString() : Carbon::now()->subWeeks(8)->toDateString();
$to = isset($data['to']) ? Carbon::parse($data['to'])->toDateString() : Carbon::today()->toDateString();
$perPage = (int) ($data['per_page'] ?? 50);
$tenant = $this->resolveTargetTenant($request);
return Tenancy::withTenant($tenant, function () use ($from, $to, $data, $perPage) {
$rows = WeeklyRoutePerformance::with('route')
->whereBetween('week_start_date', [$from, $to])
->when($data['route_id'] ?? null, fn ($q, $id) => $q->where('route_id', $id))
->orderBy('week_start_date', 'desc')
->paginate($perPage);
return $this->ok(
$rows->map(fn ($r) => [
'week_start' => $r->week_start_date->toDateString(),
'route_code' => $r->route?->code,
'on_time_rate_percent' => $r->on_time_rate_percent,
'avg_trip_duration_minutes' => $r->avg_trip_duration_minutes,
'completion_rate_percent' => $r->completion_rate_percent,
'trips_count' => $r->trips_count,
]),
null,
[
'page' => $rows->currentPage(),
'per_page' => $rows->perPage(),
'total' => $rows->total(),
'last_page' => $rows->lastPage(),
'from' => $from,
'to' => $to,
]
);
});
}
public function storeSales(Request $request): JsonResponse
{
$data = $request->validate([
'from' => ['nullable', 'date'],
'to' => ['nullable', 'date'],
'store_id' => ['nullable', 'integer'],
'tenant_id' => ['nullable', 'integer'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:500'],
]);
$from = isset($data['from']) ? Carbon::parse($data['from'])->startOfMonth()->toDateString() : Carbon::now()->subMonths(6)->startOfMonth()->toDateString();
$to = isset($data['to']) ? Carbon::parse($data['to'])->endOfMonth()->toDateString() : Carbon::today()->endOfMonth()->toDateString();
$perPage = (int) ($data['per_page'] ?? 50);
$tenant = $this->resolveTargetTenant($request);
return Tenancy::withTenant($tenant, function () use ($from, $to, $data, $perPage) {
$query = MonthlyStoreSale::with('store')
->whereBetween('month_start_date', [$from, $to])
->when($data['store_id'] ?? null, fn ($q, $id) => $q->where('store_id', $id));
$totals = [
'total_quantity' => (int) $query->sum('total_quantity_sold'),
'total_retail_centavos' => (int) $query->sum('total_retail_centavos'),
'total_commission_centavos' => (int) $query->sum('total_commission_centavos'),
];
$rows = $query->orderBy('month_start_date', 'desc')->paginate($perPage);
return $this->ok(
$rows->map(fn ($r) => [
'month' => $r->month_start_date->toDateString(),
'store_name' => $r->store?->business_name,
'quantity' => $r->total_quantity_sold,
'retail_centavos' => $r->total_retail_centavos,
'commission_centavos' => $r->total_commission_centavos,
]),
null,
array_merge($totals, [
'page' => $rows->currentPage(),
'per_page' => $rows->perPage(),
'total' => $rows->total(),
'last_page' => $rows->lastPage(),
'from' => $from,
'to' => $to,
])
);
});
}
public function complianceCsv(Request $request): StreamedResponse
{
$data = $request->validate([
'from' => ['required', 'date'],
'to' => ['required', 'date'],
'tenant_id' => ['nullable', 'integer'],
]);
$from = Carbon::parse($data['from'])->startOfDay();
$to = Carbon::parse($data['to'])->endOfDay();
$tenant = $this->resolveTargetTenant($request);
$headers = [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="verde-compliance-'.$from->toDateString().'-'.$to->toDateString().'.csv"',
];
return response()->streamDownload(function () use ($from, $to, $tenant) {
$out = fopen('php://output', 'w');
fputcsv($out, ['released_at', 'trip_number', 'dumpsite', 'permit_number', 'weight_kg', 'gate_pass', 'attendant']);
Tenancy::withTenant($tenant, function () use ($from, $to, $out) {
DumpsiteRelease::with(['dumpsite', 'trip'])
->whereBetween('released_at', [$from, $to])
->orderBy('released_at')
->chunk(500, function ($rows) use ($out) {
foreach ($rows as $r) {
fputcsv($out, [
$r->released_at?->toIso8601String(),
$r->trip?->trip_number,
$r->dumpsite?->name,
$r->dumpsite?->permit_number,
$r->weight_kg,
$r->gate_pass_number,
$r->dumpsite_attendant_name,
]);
}
});
});
fclose($out);
}, 'verde-compliance.csv', $headers);
}
public function rebuild(Request $request): JsonResponse
{
$date = $request->input('date')
? Carbon::parse($request->input('date'))
: Carbon::yesterday();
$daily = $this->aggregator->rebuildDailyCollectionStats($date);
$weekly = $this->aggregator->rebuildWeeklyRoutePerformance($date);
$monthly = $this->aggregator->rebuildMonthlyStoreSales($date);
return $this->ok([
'reference_date' => $date->toDateString(),
'daily_buckets' => $daily,
'weekly_buckets' => $weekly,
'monthly_buckets' => $monthly,
], 'Aggregations rebuilt');
}
private function resolveTargetTenant(Request $request): ?Tenant
{
if (optional($request->user())->isSuperAdmin() && $request->filled('tenant_id')) {
return Tenant::find($request->input('tenant_id'));
}
return Tenancy::current();
}
}