'essentials_categories', 'vendors' => 'essentials_vendors', 'listings' => 'essentials_listings', 'requests' => 'essentials_requests', 'reviews' => 'essentials_reviews', 'complaints' => 'essentials_complaints', 'promotions' => 'essentials_promotions', ]; public function __construct(private UuidGenerator $ids) { } public function dashboard(): array { $counts = []; foreach (self::RESOURCE_TABLES as $resource => $table) { $query = DB::table($table); if ($this->hasSoftDeletes($resource)) { $query->whereNull('deleted_at'); } $counts[$resource] = $query->count(); } return [ 'metrics' => [ 'activeCategories' => DB::table('essentials_categories')->where('is_active', true)->whereNull('deleted_at')->count(), 'approvedVendors' => DB::table('essentials_vendors')->where('status', 'approved')->whereNull('deleted_at')->count(), 'publishedListings' => DB::table('essentials_listings')->where('status', 'published')->whereNull('deleted_at')->count(), 'openRequests' => DB::table('essentials_requests')->whereNotIn('status', ['closed', 'cancelled'])->whereNull('deleted_at')->count(), 'openComplaints' => DB::table('essentials_complaints')->whereIn('status', ['open', 'investigating'])->count(), 'activePromotions' => DB::table('essentials_promotions')->where('is_active', true)->count(), ], 'counts' => $counts, 'recentRequests' => $this->list('requests', ['per_page' => 5])['items'], 'featuredListings' => $this->list('listings', ['per_page' => 5, 'status' => 'published'])['items'], ]; } public function list(string $resource, array $filters = []): array { $table = $this->tableFor($resource); $page = max(1, (int) ($filters['page'] ?? 1)); $perPage = min(100, max(1, (int) ($filters['per_page'] ?? 25))); $search = trim((string) ($filters['search'] ?? '')); $status = trim((string) ($filters['status'] ?? '')); $query = DB::table($table); if ($this->hasSoftDeletes($resource)) { $query->whereNull('deleted_at'); } if ($status !== '' && $status !== 'all') { if ($resource === 'categories') { $query->where('is_active', in_array($status, ['active', '1', 'true'], true)); } elseif ($resource === 'promotions') { $query->where('is_active', in_array($status, ['active', '1', 'true'], true)); } else { $query->where('status', $status); } } if ($search !== '') { $this->applySearch($query, $resource, $search); } $total = (clone $query)->count(); $rows = $query ->orderByDesc($this->orderColumn($resource)) ->offset(($page - 1) * $perPage) ->limit($perPage) ->get(); return [ 'items' => $rows->map(fn (object $row) => $this->recordPayload($resource, $row))->all(), 'total' => $total, 'page' => $page, 'per_page' => $perPage, ]; } public function get(string $resource, string $id): ?array { $table = $this->tableFor($resource); $query = DB::table($table)->where('id', $id); if ($this->hasSoftDeletes($resource)) { $query->whereNull('deleted_at'); } $row = $query->first(); return $row === null ? null : $this->recordPayload($resource, $row); } public function save(string $resource, array $input): array { $table = $this->tableFor($resource); $id = (string) ($input['id'] ?? $input[$this->idKey($resource)] ?? $this->ids->generate()); $now = now(); $payload = $this->writePayload($resource, $input, $id, $now); $exists = DB::table($table)->where('id', $id)->exists(); if ($exists) { $updates = $payload; if ($resource !== 'reviews') { $updates['updated_at'] = $now; } DB::table($table)->where('id', $id)->update($updates); } else { $insert = array_merge($payload, ['created_at' => $now]); if ($resource !== 'reviews') { $insert['updated_at'] = $now; } DB::table($table)->insert($insert); } return $this->get($resource, $id) ?? []; } public function transition(string $resource, string $id, string $status, ?string $note = null, ?string $actorId = null): array { $table = $this->tableFor($resource); $existing = DB::table($table)->where('id', $id)->first(); if ($existing === null) { return []; } if ($resource === 'categories') { DB::table($table)->where('id', $id)->update(['is_active' => $status === 'active', 'updated_at' => now()]); } elseif ($resource === 'promotions') { DB::table($table)->where('id', $id)->update(['is_active' => $status === 'active', 'updated_at' => now()]); } else { $updates = ['status' => $status]; if ($resource !== 'reviews') { $updates['updated_at'] = now(); } DB::table($table)->where('id', $id)->update($updates); } if ($resource === 'requests') { DB::table('essentials_request_status_history')->insert([ 'id' => $this->ids->generate(), 'request_id' => $id, 'from_status' => $existing->status ?? null, 'to_status' => $status, 'changed_by' => $actorId, 'note' => $note, 'created_at' => now(), ]); } return $this->get($resource, $id) ?? []; } private function writePayload(string $resource, array $input, string $id, mixed $now): array { return match ($resource) { 'categories' => [ 'id' => $id, 'parent_id' => $input['parent_id'] ?? $input['parentId'] ?? null, 'name' => (string) ($input['name'] ?? $input['title'] ?? 'Essentials category'), 'slug' => (string) ($input['slug'] ?? $this->slug($input['name'] ?? $input['title'] ?? $id)), 'description' => $input['description'] ?? null, 'icon' => $input['icon'] ?? null, 'sort_order' => (int) ($input['sort_order'] ?? $input['sortOrder'] ?? 0), 'is_active' => (bool) ($input['is_active'] ?? $input['isActive'] ?? true), 'row_version' => (int) ($input['row_version'] ?? 1), ], 'vendors' => [ 'id' => $id, 'vendor_number' => $input['vendor_number'] ?? $input['vendorNumber'] ?? null, 'vendor_type' => $input['vendor_type'] ?? $input['vendorType'] ?? 'general_marketplace_seller', 'category_id' => $input['category_id'] ?? $input['categoryId'] ?? null, 'business_name' => (string) ($input['business_name'] ?? $input['businessName'] ?? $input['title'] ?? 'Essentials vendor'), 'country' => $input['country'] ?? null, 'city' => $input['city'] ?? null, 'business_email' => $input['business_email'] ?? $input['businessEmail'] ?? null, 'business_phone' => $input['business_phone'] ?? $input['businessPhone'] ?? null, 'business_description' => $input['business_description'] ?? $input['description'] ?? null, 'services_offered' => $input['services_offered'] ?? $input['servicesOffered'] ?? null, 'status' => $input['status'] ?? 'draft', 'risk_level' => $input['risk_level'] ?? $input['riskLevel'] ?? null, 'row_version' => (int) ($input['row_version'] ?? 1), ], 'listings' => [ 'id' => $id, 'category_id' => (string) ($input['category_id'] ?? $input['categoryId'] ?? $this->defaultCategoryId()), 'vendor_id' => $input['vendor_id'] ?? $input['vendorId'] ?? null, 'title' => (string) ($input['title'] ?? 'Essentials listing'), 'slug' => (string) ($input['slug'] ?? $this->slug($input['title'] ?? $id)), 'description' => $input['description'] ?? null, 'listing_type' => $input['listing_type'] ?? $input['listingType'] ?? 'fixed_price', 'price_amount' => $input['price_amount'] ?? $input['priceAmount'] ?? null, 'price_max' => $input['price_max'] ?? $input['priceMax'] ?? null, 'currency' => $input['currency'] ?? 'KES', 'member_offer_text' => $input['member_offer_text'] ?? $input['memberOfferText'] ?? null, 'delivery_timeline' => $input['delivery_timeline'] ?? $input['deliveryTimeline'] ?? null, 'location_served' => $input['location_served'] ?? $input['locationServed'] ?? null, 'status' => $input['status'] ?? 'draft', 'published_at' => ($input['status'] ?? null) === 'published' ? $now : null, 'row_version' => (int) ($input['row_version'] ?? 1), ], 'requests' => [ 'id' => $id, 'reference_no' => $input['reference_no'] ?? $input['referenceNo'] ?? 'ESS-'.strtoupper(substr($id, -6)), 'member_id' => (string) ($input['member_id'] ?? $input['memberId'] ?? '00000000-0000-7000-8000-000000000101'), 'listing_id' => $input['listing_id'] ?? $input['listingId'] ?? null, 'category_id' => (string) ($input['category_id'] ?? $input['categoryId'] ?? $this->defaultCategoryId()), 'request_type' => $input['request_type'] ?? $input['requestType'] ?? 'custom_request', 'title' => (string) ($input['title'] ?? 'Essentials request'), 'status' => $input['status'] ?? 'draft', 'budget_amount' => $input['budget_amount'] ?? $input['budgetAmount'] ?? null, 'currency' => $input['currency'] ?? 'KES', 'timeline' => $input['timeline'] ?? null, 'location' => $input['location'] ?? null, 'contact_details' => $this->jsonOrNull($input['contact_details'] ?? $input['contactDetails'] ?? null), 'beneficiary_details' => $this->jsonOrNull($input['beneficiary_details'] ?? $input['beneficiaryDetails'] ?? null), 'form_payload' => $this->jsonOrNull($input['form_payload'] ?? $input['formPayload'] ?? null), 'priority' => $input['priority'] ?? 'normal', 'assigned_admin_id' => $input['assigned_admin_id'] ?? $input['assignedAdminId'] ?? null, 'submitted_at' => in_array(($input['status'] ?? ''), ['submitted', 'in_progress'], true) ? $now : null, 'row_version' => (int) ($input['row_version'] ?? 1), ], 'complaints' => [ 'id' => $id, 'request_id' => $input['request_id'] ?? $input['requestId'] ?? null, 'review_id' => $input['review_id'] ?? $input['reviewId'] ?? null, 'member_id' => (string) ($input['member_id'] ?? $input['memberId'] ?? '00000000-0000-7000-8000-000000000101'), 'vendor_id' => $input['vendor_id'] ?? $input['vendorId'] ?? null, 'subject' => (string) ($input['subject'] ?? $input['title'] ?? 'Essentials complaint'), 'description' => (string) ($input['description'] ?? ''), 'severity' => $input['severity'] ?? 'medium', 'status' => $input['status'] ?? 'open', 'resolution_note' => $input['resolution_note'] ?? $input['resolutionNote'] ?? null, 'row_version' => (int) ($input['row_version'] ?? 1), ], 'reviews' => [ 'id' => $id, 'request_id' => (string) ($input['request_id'] ?? $input['requestId'] ?? '00000000-0000-7000-8000-000000003004'), 'listing_id' => $input['listing_id'] ?? $input['listingId'] ?? null, 'vendor_id' => $input['vendor_id'] ?? $input['vendorId'] ?? null, 'member_id' => (string) ($input['member_id'] ?? $input['memberId'] ?? '00000000-0000-7000-8000-000000000101'), 'service_quality' => (int) ($input['service_quality'] ?? $input['serviceQuality'] ?? 5), 'responsiveness' => (int) ($input['responsiveness'] ?? 5), 'reliability' => (int) ($input['reliability'] ?? 5), 'value_for_money' => (int) ($input['value_for_money'] ?? $input['valueForMoney'] ?? 5), 'overall_satisfaction' => (int) ($input['overall_satisfaction'] ?? $input['overallSatisfaction'] ?? 5), 'would_recommend' => (bool) ($input['would_recommend'] ?? $input['wouldRecommend'] ?? true), 'written_feedback' => $input['written_feedback'] ?? $input['writtenFeedback'] ?? null, 'complaint_flag' => (bool) ($input['complaint_flag'] ?? $input['complaintFlag'] ?? false), 'visibility' => $input['visibility'] ?? 'internal', 'status' => $input['status'] ?? 'pending', ], 'promotions' => [ 'id' => $id, 'listing_id' => $input['listing_id'] ?? $input['listingId'] ?? null, 'vendor_id' => $input['vendor_id'] ?? $input['vendorId'] ?? null, 'category_id' => $input['category_id'] ?? $input['categoryId'] ?? null, 'title' => (string) ($input['title'] ?? 'Essentials promotion'), 'offer_type' => $input['offer_type'] ?? $input['offerType'] ?? 'discount', 'value' => $input['value'] ?? null, 'description' => $input['description'] ?? null, 'starts_at' => $input['starts_at'] ?? $input['startsAt'] ?? null, 'ends_at' => $input['ends_at'] ?? $input['endsAt'] ?? null, 'is_active' => (bool) ($input['is_active'] ?? $input['isActive'] ?? true), 'row_version' => (int) ($input['row_version'] ?? 1), ], default => ['id' => $id], }; } private function recordPayload(string $resource, object $row): array { $raw = (array) $row; $title = match ($resource) { 'categories' => $row->name, 'vendors' => $row->business_name, 'listings' => $row->title, 'requests' => $row->title, 'reviews' => 'Review '.$row->id, 'complaints' => $row->subject, 'promotions' => $row->title, default => $row->id, }; $status = match ($resource) { 'categories' => ((int) $row->is_active) === 1 ? 'active' : 'inactive', 'promotions' => ((int) $row->is_active) === 1 ? 'active' : 'inactive', default => (string) ($row->status ?? 'active'), }; $summary = match ($resource) { 'categories' => (string) ($row->description ?? ''), 'vendors' => (string) ($row->services_offered ?? $row->business_description ?? ''), 'listings' => trim((string) ($row->description ?? '').' '.($row->price_amount ? $row->currency.' '.$row->price_amount : '')), 'requests' => trim(($row->reference_no ?? '').' '.($row->budget_amount ? $row->currency.' '.$row->budget_amount : '')), 'reviews' => (string) ($row->written_feedback ?? 'Overall satisfaction '.$row->overall_satisfaction), 'complaints' => (string) ($row->description ?? ''), 'promotions' => (string) ($row->description ?? $row->offer_type), default => '', }; return [ 'id' => $row->id, 'title' => $title, 'status' => $status, 'summary' => $summary, 'updatedAt' => (string) ($row->updated_at ?? $row->created_at ?? ''), 'resource' => $resource, 'raw' => $raw, ] + $raw; } private function applySearch(mixed $query, string $resource, string $search): void { $columns = match ($resource) { 'categories' => ['name', 'slug', 'description'], 'vendors' => ['business_name', 'vendor_number', 'business_email', 'services_offered'], 'listings' => ['title', 'slug', 'description', 'location_served'], 'requests' => ['reference_no', 'title', 'location', 'timeline'], 'reviews' => ['written_feedback'], 'complaints' => ['subject', 'description', 'severity'], 'promotions' => ['title', 'description', 'offer_type'], default => ['id'], }; $query->where(function ($inner) use ($columns, $search) { foreach ($columns as $column) { $inner->orWhere($column, 'like', '%'.$search.'%'); } }); } private function tableFor(string $resource): string { if (! array_key_exists($resource, self::RESOURCE_TABLES)) { abort(404, 'Unknown Essentials resource.'); } return self::RESOURCE_TABLES[$resource]; } private function idKey(string $resource): string { return rtrim($resource, 's').'_id'; } private function hasSoftDeletes(string $resource): bool { return in_array($resource, ['categories', 'vendors', 'listings', 'requests'], true); } private function orderColumn(string $resource): string { return $resource === 'reviews' ? 'created_at' : 'updated_at'; } private function defaultCategoryId(): string { $category = DB::table('essentials_categories')->whereNull('deleted_at')->orderBy('sort_order')->first(); return (string) ($category->id ?? '00000000-0000-7000-8000-000000003001'); } private function slug(mixed $value): string { $slug = strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', (string) $value), '-')); return $slug !== '' ? $slug : strtolower(substr($this->ids->generate(), -12)); } private function jsonOrNull(mixed $value): ?string { if ($value === null || $value === '') { return null; } return is_string($value) ? $value : json_encode($value); } }