feat: implement team standby status, helper persistence, and store portal foundations

This commit is contained in:
Developer
2026-07-02 12:02:13 +08:00
parent a9a1df93bd
commit a1470eb85e
32 changed files with 1371 additions and 82 deletions

View File

@@ -108,6 +108,7 @@ class AdminPartnerStoreController extends ApiController
$data = $request->validate([
'quantity' => ['required', 'integer', 'min:1', 'max:50000'],
'wholesale_price_centavos' => ['required', 'integer', 'min:0'],
'is_prepaid' => ['nullable', 'boolean'],
]);
if ($store->status !== PartnerStore::STATUS_ACTIVE) {
@@ -119,7 +120,8 @@ class AdminPartnerStoreController extends ApiController
(int) $data['quantity'],
(int) $data['wholesale_price_centavos'],
null,
$request->user()
$request->user(),
(bool) ($data['is_prepaid'] ?? false)
);
return $this->created([

View File

@@ -20,7 +20,7 @@ class AdminTeamController extends ApiController
public function index(Request $request): JsonResponse
{
$request->validate([
'status' => ['nullable', 'in:active,inactive'],
'status' => ['nullable', 'in:active,inactive,standby'],
'area_id' => ['nullable', 'integer'],
'q' => ['nullable', 'string', 'max:100'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:500'],
@@ -28,7 +28,7 @@ class AdminTeamController extends ApiController
$perPage = (int) $request->input('per_page', 25);
$teams = CollectionTeam::query()
->with(['area', 'driver', 'scanner', 'truck'])
->with(['area', 'driver', 'scanner', 'truck', 'helpers.user', 'currentTrip'])
->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status')))
->when($request->filled('area_id'), fn ($q) => $q->where('area_id', $request->integer('area_id')))
->when($request->filled('q'), fn ($q) => $q->where('name', 'like', '%'.$request->string('q').'%'))
@@ -77,7 +77,7 @@ class AdminTeamController extends ApiController
}
}
unset($data['helper_ids'], $data['override_conflicts'], $data['helper_count'], $data['helper_assignment_mode']);
unset($data['helper_ids'], $data['override_conflicts'], $data['helper_assignment_mode']);
$team = DB::transaction(function () use ($data, $helperIds) {
$team = CollectionTeam::create($data);
@@ -105,7 +105,7 @@ class AdminTeamController extends ApiController
public function show(CollectionTeam $team): JsonResponse
{
$team->load(['area', 'driver', 'scanner', 'truck', 'helpers.user']);
$team->load(['area', 'driver', 'scanner', 'truck', 'helpers.user', 'currentTrip']);
return $this->ok(new CollectionTeamResource($team));
}
@@ -143,7 +143,7 @@ class AdminTeamController extends ApiController
}
}
unset($data['helper_ids'], $data['override_conflicts'], $data['helper_count'], $data['helper_assignment_mode']);
unset($data['helper_ids'], $data['override_conflicts'], $data['helper_assignment_mode']);
DB::transaction(function () use ($team, $data, $helperIds) {
$team->update($data);

View File

@@ -35,6 +35,8 @@ class TripDetourController extends Controller
'created_at' => now(),
]);
$trip->update(['is_detouring' => true]);
return response()->json(['message' => 'Detour to dumpsite logged.']);
}
@@ -63,6 +65,8 @@ class TripDetourController extends Controller
'created_at' => now(),
]);
$trip->update(['is_detouring' => false]);
return response()->json(['message' => 'Resuming route logged.']);
}
}

View File

@@ -0,0 +1,184 @@
<?php
namespace App\Http\Controllers\Api\V1\Store;
use App\Http\Controllers\Api\V1\ApiController;
use App\Models\PartnerStore;
use App\Models\User;
use App\Services\Store\StoreOperations;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class StorePortalController extends ApiController
{
/**
* Get the store associated with the authenticated user.
*/
protected function getStore(): PartnerStore
{
/** @var User $user */
$user = auth()->user();
$store = PartnerStore::where('owner_user_id', $user->id)->first();
if (!$store) {
abort(403, 'No partner store associated with this account.');
}
return $store;
}
/**
* Get dashboard summary data.
*/
public function dashboard(): JsonResponse
{
$store = $this->getStore();
$inventory = $store->inventory;
$data = [
'business_name' => $store->business_name,
'inventory_balance' => $inventory ? (int) $inventory->current_code_balance : 0,
'prepaid_balance' => $inventory ? (int) $inventory->prepaid_balance : 0,
'total_sales_count' => (int) $store->sales()->count(),
'status' => $store->status,
];
return $this->ok($data);
}
/**
* Get financial analytics for the store.
*/
public function analytics(): JsonResponse
{
$store = $this->getStore();
// Sums
$totalRetail = $store->sales()->sum('retail_price_centavos');
$totalWholesale = $store->purchases()->sum('wholesale_price_centavos');
$totalCommission = $store->sales()->sum('commission_centavos');
// Calculate settlement balance
$totalSettled = $store->settlements()->sum('amount_centavos');
$amountOwed = ($totalRetail - $totalCommission) - $totalSettled;
return $this->ok([
'total_retail_revenue_centavos' => (int) $totalRetail,
'total_wholesale_cost_centavos' => (int) $totalWholesale,
'total_commission_earned_centavos' => (int) $totalCommission,
'amount_owed_to_lgu_centavos' => (int) $amountOwed,
'gross_profit_centavos' => (int) ($totalRetail - $totalWholesale),
]);
}
/**
* Record a retail sale to a resident.
*/
public function recordSale(Request $request, StoreOperations $operations): JsonResponse
{
$store = $this->getStore();
$request->validate([
'household_id' => 'required|exists:households,id',
'quantity' => 'required|integer|min:1',
'serial_number' => 'nullable|string|exists:qr_codes,serial',
]);
try {
$sale = $operations->sellToHousehold(
$store,
\App\Models\Household::findOrFail($request->household_id),
$request->quantity,
config('qr.default_retail_price_per_code_centavos'),
$request->serial_number
);
// Fetch the serials that were just activated for this sale
$codes = \App\Models\QrCode::whereIn('id', $sale->id ? [$sale->id] : []) // Wait, StoreSale doesn't link to QrCode directly?
// I need to find the codes that were activated.
// QrCode table has assigned_to_household_id.
->where('assigned_to_household_id', $request->household_id)
->where('status', \App\States\QrCode\Active::$name)
->latest('activated_at')
->limit($request->quantity)
->get();
$qrData = $codes->map(function ($code) {
$qr = \Endroid\QrCode\QrCode::create($code->serial)
->setSize(300)
->setMargin(10);
$writer = new \Endroid\QrCode\Writer\PngWriter();
return [
'serial' => $code->serial,
'qr_base64' => base64_encode($writer->write($qr)->getString())
];
});
return $this->created([
'sale' => $sale,
'codes' => $qrData
], 'Sale recorded successfully.');
} catch (\Exception $e) {
return $this->fail($e->getMessage(), null, 422);
}
}
/**
* Get sales history for the store.
*/
public function salesHistory(): JsonResponse
{
$store = $this->getStore();
$sales = $store->sales()
->with('household.head')
->latest()
->paginate(15);
return $this->ok($sales);
}
/**
* Get inventory adjustment and purchase history.
*/
public function inventoryHistory(): JsonResponse
{
$store = $this->getStore();
$history = \App\Models\StoreInventoryAdjustment::query()
->where('store_id', $store->id)
->with('user')
->latest()
->paginate(15);
return $this->ok($history);
}
/**
* Search for households to record a sale.
*/
public function searchHouseholds(Request $request): JsonResponse
{
$request->validate([
'q' => 'required|string|min:2',
]);
$term = '%' . $request->string('q') . '%';
$households = \App\Models\Household::query()
->with('head')
->where('verification_status', \App\Models\Household::VERIFICATION_APPROVED)
->where(function($q) use ($term) {
$q->where('address_line', 'like', $term)
->orWhereHas('head', function($h) use ($term) {
$h->where('first_name', 'like', $term)
->orWhere('last_name', 'like', $term);
});
})
->limit(10)
->get();
return $this->ok($households);
}
}

View File

@@ -85,6 +85,22 @@ class SuperAdminUserController extends ApiController
'language' => $data['preferred_language'] ?? 'en',
]);
// If store_partner, auto-create a shell store so they can access the dashboard
if ($data['role'] === User::ROLE_STORE_PARTNER) {
$store = \App\Models\PartnerStore::create([
'tenant_id' => $user->tenant_id,
'owner_user_id' => $user->id,
'business_name' => $user->first_name . "'s Store",
'status' => \App\Models\PartnerStore::STATUS_ACTIVE,
'commission_rate_percent' => 10, // Default 10%
]);
\App\Models\StoreInventory::create([
'store_id' => $store->id,
'current_code_balance' => 0,
]);
}
return $user;
});

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Store;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class StoreDashboardController extends Controller
{
public function index()
{
return view('store.dashboard');
}
public function sales()
{
return view('store.sales');
}
public function inventory()
{
return view('store.inventory');
}
public function financials()
{
return view('store.financials');
}
}