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.
This commit is contained in:
@@ -7,6 +7,8 @@ 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;
|
||||
@@ -23,33 +25,48 @@ class AdminReportController extends ApiController
|
||||
'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);
|
||||
|
||||
$rows = DailyCollectionStat::query()
|
||||
->whereBetween('date', [$from, $to])
|
||||
->when($data['barangay_id'] ?? null, fn ($q, $id) => $q->where('barangay_id', $id))
|
||||
->orderBy('date')
|
||||
->get();
|
||||
$tenant = $this->resolveTargetTenant($request);
|
||||
|
||||
return $this->ok([
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'series' => $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,
|
||||
]),
|
||||
'totals' => [
|
||||
'total_scans' => (int) $rows->sum('total_scans'),
|
||||
'total_weight_kg' => (int) $rows->sum('total_weight_kg'),
|
||||
'unique_households' => (int) $rows->sum('unique_households'),
|
||||
],
|
||||
]);
|
||||
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
|
||||
@@ -58,28 +75,42 @@ class AdminReportController extends ApiController
|
||||
'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);
|
||||
|
||||
$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')
|
||||
->get();
|
||||
$tenant = $this->resolveTargetTenant($request);
|
||||
|
||||
return $this->ok([
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'series' => $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,
|
||||
]),
|
||||
]);
|
||||
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
|
||||
@@ -88,32 +119,47 @@ class AdminReportController extends ApiController
|
||||
'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);
|
||||
|
||||
$rows = MonthlyStoreSale::with('store')
|
||||
->whereBetween('month_start_date', [$from, $to])
|
||||
->when($data['store_id'] ?? null, fn ($q, $id) => $q->where('store_id', $id))
|
||||
->orderBy('month_start_date')
|
||||
->get();
|
||||
$tenant = $this->resolveTargetTenant($request);
|
||||
|
||||
return $this->ok([
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'series' => $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,
|
||||
]),
|
||||
'totals' => [
|
||||
'quantity' => (int) $rows->sum('total_quantity_sold'),
|
||||
'retail_centavos' => (int) $rows->sum('total_retail_centavos'),
|
||||
'commission_centavos' => (int) $rows->sum('total_commission_centavos'),
|
||||
],
|
||||
]);
|
||||
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
|
||||
@@ -121,35 +167,40 @@ class AdminReportController extends ApiController
|
||||
$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) {
|
||||
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']);
|
||||
|
||||
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,
|
||||
]);
|
||||
}
|
||||
});
|
||||
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);
|
||||
@@ -172,4 +223,13 @@ class AdminReportController extends ApiController
|
||||
'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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user