10, 'search' => '', 'orderBy' => 'id', 'order' => 'desc', 'with_deleted' => false, ]; return array_merge($defaultArgs, $filterData); } private function getFoodItemQuery(): Builder { return $this->getQuery() ->select( "{$this->table}.id", "{$this->table}.restaurant_id", "{$this->table}.category_id", "{$this->table}.menu_category_id", "{$this->table}.menu_section_id", "{$this->table}.name", "{$this->table}.slug", "{$this->table}.description", "{$this->table}.food_type", "{$this->table}.points", "{$this->table}.is_featured", "{$this->table}.is_party", "{$this->table}.is_dinner", "{$this->table}.is_lunch", "{$this->table}.is_popular_item", "{$this->table}.is_chef_special", "{$this->table}.image", "{$this->table}.prep_time", "{$this->table}.cooking_time", "{$this->table}.vat", "{$this->table}.pst", "{$this->table}.ingredients_cost", "{$this->table}.allergens", "{$this->table}.notes", "{$this->table}.status", "{$this->table}.created_at", "{$this->table}.updated_at", "{$this->table}.deleted_at", ); } protected function filterSearchQuery(Builder|EloquentBuilder $query, string $searchedText): Builder { $searchable = "%$searchedText%"; return $query->where("{$this->table}.name", 'LIKE', $searchable) ->orWhere("{$this->table}.status", 'LIKE', $searchable); } public function getAll(array $filterData = []): Paginator { $filter = $this->getFilterData($filterData); $query = $this->getFoodItemQuery(); if (! $filter['with_deleted']) { $query->whereNull("{$this->table}.deleted_at"); } if (! empty($filter['search'])) { $query = $this->filterSearchQuery($query, $filter['search']); } return $query ->orderBy($filter['orderBy'], $filter['order']) ->paginate($filter['perPage']); } public function getCount(array $filterData = []): int { $filter = $this->getFilterData($filterData); $query = $this->getQuery(); if (! $filter['with_deleted']) { $query->whereNull("{$this->table}.deleted_at"); } return $query->count(); } /** * @throws Exception */ public function getByColumn(string $columnName, $columnValue, array $selects = ['*']): ?object { $item = $this->getFoodItemQuery() ->where($columnName, $columnValue) ->first($selects); if (empty($item)) { throw new Exception( $this->getExceptionMessage(static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE), Response::HTTP_NOT_FOUND ); } return $item; } /** * @throws Exception */ public function create(array $data): object { return DB::transaction(function () use ($data) { $variants = $data['variants'] ?? []; unset($data['variants']); $data = $this->prepareForDB($data); /** @var FoodItem $food */ $food = FoodItem::create($data); $this->syncVariants($food, $variants); return $food->load('variants'); }); } /** * @throws Exception */ public function update(int $id, array $data): object { return DB::transaction(function () use ($id, $data) { /** @var FoodItem $food */ $food = FoodItem::findOrFail($id); // Extract variants array $variants = $data['variants'] ?? []; unset($data['variants']); // Update food main data $data = $this->prepareForDB($data, $food); $food->update($data); // Sync only changed variants $this->syncVariants($food, $variants); return $food->load('variants'); }); } /** * Prepare data for DB insert/update */ public function prepareForDB(array $data, ?object $item = null): array { if (! empty($data['prep_time'])) { $data['prep_time'] = date('H:i:s', strtotime($data['prep_time'])); } if (! empty($data['cooking_time'])) { $data['cooking_time'] = date('H:i:s', strtotime($data['cooking_time'])); } $data = parent::prepareForDB($data, $item); if (! empty($data['image']) && $data['image'] instanceof \Illuminate\Http\UploadedFile) { $data['image'] = fileUploader('food_items/', 'png', $data['image'], $item->image ?? null); } $data['restaurant_id'] = $this->getCurrentRestaurantId(); $data['updated_at'] = now(); if (empty($item)) { $data['created_at'] = now(); $data['status'] = 1; } return $data; } /** * Sync food variants safely */ private function syncVariants(FoodItem $food, array $variants): void { $existingIds = $food->variants()->pluck('id')->toArray(); $receivedIds = []; $defaultMarked = false; foreach ($variants as $variant) { // Determine default $isDefault = ! $defaultMarked && ($variant['is_default'] ?? false); $defaultMarked = $defaultMarked || $isDefault; // If variant has ID → update if (! empty($variant['id'])) { $receivedIds[] = $variant['id']; $food->variants() ->where('id', $variant['id']) ->update([ 'restaurant_id' => $food->restaurant_id, 'name' => $variant['name'], 'sku' => $variant['sku'] ?? $this->generateSku($food, $variant['name']), 'price' => $variant['price'], 'offer_price' => $variant['offer_price'] ?? null, 'discount' => $variant['discount'] ?? 0, 'unit_id' => $variant['unit_id'] ?? 'pcs', 'stock_tracking' => $variant['stock_tracking'] ?? true, 'ingredients_cost' => $variant['ingredients_cost'] ?? 0, 'profit_margin' => $variant['profit_margin'] ?? 0, 'alert_stock_quantity' => $variant['alert_stock_quantity'] ?? null, 'combo_type' => $variant['combo_type'] ?? 'single', 'is_active_offer' => $variant['is_active_offer'] ?? false, 'is_default' => $isDefault, 'updated_at' => now(), ]); continue; } // If no ID → create new variant $newVariant = $food->variants()->create([ 'restaurant_id' => $food->restaurant_id, 'name' => $variant['name'], 'sku' => $this->generateSku($food, $variant['name']), 'price' => $variant['price'], 'offer_price' => $variant['offer_price'] ?? null, 'discount' => $variant['discount'] ?? 0, 'unit_id' => $variant['unit_id'] ?? 'pcs', 'stock_tracking' => $variant['stock_tracking'] ?? true, 'ingredients_cost' => $variant['ingredients_cost'] ?? 0, 'profit_margin' => $variant['profit_margin'] ?? 0, 'alert_stock_quantity' => $variant['alert_stock_quantity'] ?? null, 'combo_type' => $variant['combo_type'] ?? 'single', 'is_active_offer' => $variant['is_active_offer'] ?? false, 'is_default' => $isDefault, 'status' => 1, 'created_at' => now(), ]); $receivedIds[] = $newVariant->id; } // Delete only removed variants $toDelete = array_diff($existingIds, $receivedIds); if (! empty($toDelete)) { $food->variants()->whereIn('id', $toDelete)->delete(); } } // private function syncVariants(FoodItem $food, array $variants): void // { // $defaultMarked = false; // foreach ($variants as $variant) { // $isDefault = ! $defaultMarked && ($variant['is_default'] ?? false); // $defaultMarked = $defaultMarked || $isDefault; // $food->variants()->create([ // 'restaurant_id' => $food->restaurant_id, // 'name' => $variant['name'], // 'sku' => $this->generateSku($food, $variant['name']), // 'price' => $variant['price'], // 'offer_price' => $variant['offer_price'] ?? null, // 'discount' => $variant['discount'] ?? 0, // 'unit_id' => $variant['unit_id'] ?? 'pcs', // 'stock_tracking' => $variant['stock_tracking'] ?? true, // 'ingredients_cost' => $variant['ingredients_cost'] ?? 0, // 'profit_margin' => $variant['profit_margin'] ?? 0, // 'alert_stock_quantity' => $variant['alert_stock_quantity'] ?? null, // 'combo_type' => $variant['combo_type'] ?? 'single', // 'is_active_offer' => $variant['is_active_offer'] ?? false, // 'is_default' => $isDefault, // 'status' => 1, // 'created_at' => now(), // ]); // } // } /** * Simple SKU generator */ private function generateSku(FoodItem $food, string $variantName): string { return strtoupper(substr($food->name, 0, 3)).'-'.strtoupper(substr($variantName, 0, 3)).'-'.rand(1000, 9999); } protected function getExceptionMessages(): array { return [ static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE => 'FoodItem does not exist.', static::MESSAGE_ITEM_COULD_NOT_BE_DELETED => 'FoodItem could not be deleted.', ]; } }