Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
71.48% |
203 / 284 |
|
36.59% |
15 / 41 |
CRAP | n/a |
0 / 0 |
|
| CurrencyFormate | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| moneyIcon | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addIconSVG | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| totalPurchase | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| totalSale | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| totalPurchaseDue | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| totalSaleDue | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| totalCustomer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| totalSupplier | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| totalSaleInvoice | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| totalPurchaseInvoice | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| stockInSum | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| stockOutSum | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| saleQTY | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| purchaseQTY | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| detailsInfo | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| PurchaseDetailsInfo | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| ColorName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| setting | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| social_setting | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| saleList | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| deliveredSale | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| colors | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| sizes | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| brands | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| menus | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| categories | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| products | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| discount_calculate | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| colorSize | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| prev_route | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| sections | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| order_by_status | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| payment_type | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| admin_menu_route | |
33.33% |
1 / 3 |
|
0.00% |
0 / 1 |
5.67 | |||
| admin_menu_is_active | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
| admin_menu_group_open | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
| storefront_theme_view | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
5.12 | |||
| admin_menu | |
96.18% |
126 / 131 |
|
0.00% |
0 / 1 |
10 | |||
| admin_menu_apply_permissions | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
10 | |||
| admin_menu_apply_features | |
94.74% |
18 / 19 |
|
0.00% |
0 / 1 |
9.01 | |||
| 1 | <?php |
| 2 | |
| 3 | use App\Models\admin\Customer; |
| 4 | use App\Models\admin\Product; |
| 5 | use App\Models\admin\Purchase; |
| 6 | use App\Models\admin\PurchaseDetails; |
| 7 | use App\Models\admin\PurchasePayment; |
| 8 | use App\Models\admin\Sale; |
| 9 | use App\Models\admin\SaleDetalis; |
| 10 | use App\Models\admin\SalePayment; |
| 11 | use App\Models\admin\Stock; |
| 12 | use App\Models\admin\Supplier; |
| 13 | use App\Models\Brand; |
| 14 | use App\Models\Category; |
| 15 | use App\Models\ChatConversation; |
| 16 | use App\Models\Color; |
| 17 | use App\Models\Order; |
| 18 | use App\Models\PaymentType; |
| 19 | use App\Models\Section; |
| 20 | use App\Models\Setting; |
| 21 | use App\Models\Size; |
| 22 | use App\Models\SocialSetting; |
| 23 | use App\Service\CacheService; |
| 24 | use App\Services\Feature\FeatureService; |
| 25 | use App\Services\Permission\PermissionService; |
| 26 | use App\Support\LegacySettingProxy; |
| 27 | use Illuminate\Support\Facades\Route; |
| 28 | |
| 29 | function CurrencyFormate($value) |
| 30 | { |
| 31 | return moneyIcon() . number_format($value, 2); |
| 32 | } |
| 33 | |
| 34 | if (! function_exists('moneyIcon')) { |
| 35 | function moneyIcon() |
| 36 | { |
| 37 | return settings('general.currency_symbol', '৳') . ' '; |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | if (! function_exists('addIconSVG')) { |
| 42 | // Kept for backward compatibility (in case a view still calls it), but no |
| 43 | // longer hotlinks the demo template vendor's CDN — live pages now render |
| 44 | // a local FontAwesome "plus" icon instead (see page_header.blade.php). |
| 45 | function addIconSVG() |
| 46 | { |
| 47 | return ''; |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | if (! function_exists('totalPurchase')) { |
| 52 | function totalPurchase() |
| 53 | { |
| 54 | |
| 55 | return PurchasePayment::query()->get()->sum('total'); |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | if (! function_exists('totalSale')) { |
| 60 | function totalSale() |
| 61 | { |
| 62 | return SalePayment::query()->get()->sum('total'); |
| 63 | } |
| 64 | } |
| 65 | if (! function_exists('totalPurchaseDue')) { |
| 66 | function totalPurchaseDue() |
| 67 | { |
| 68 | return PurchasePayment::query()->get()->sum('due'); |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | if (! function_exists('totalSaleDue')) { |
| 73 | function totalSaleDue() |
| 74 | { |
| 75 | return SalePayment::query()->get()->sum('due'); |
| 76 | } |
| 77 | } |
| 78 | if (! function_exists('totalCustomer')) { |
| 79 | function totalCustomer() |
| 80 | { |
| 81 | return Customer::query()->get()->count(); |
| 82 | } |
| 83 | } |
| 84 | if (! function_exists('totalSupplier')) { |
| 85 | function totalSupplier() |
| 86 | { |
| 87 | return Supplier::query()->get()->count(); |
| 88 | } |
| 89 | } |
| 90 | if (! function_exists('totalSaleInvoice')) { |
| 91 | function totalSaleInvoice() |
| 92 | { |
| 93 | return Sale::query()->get()->count(); |
| 94 | } |
| 95 | } |
| 96 | if (! function_exists('totalPurchaseInvoice')) { |
| 97 | function totalPurchaseInvoice() |
| 98 | { |
| 99 | return Purchase::query()->get()->count(); |
| 100 | } |
| 101 | } |
| 102 | if (! function_exists('stockInSum')) { |
| 103 | function stockInSum($id) |
| 104 | { |
| 105 | return Stock::query()->where('product_id', $id)->get()->sum('stock_in'); |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | if (! function_exists('stockOutSum')) { |
| 110 | function stockOutSum($id) |
| 111 | { |
| 112 | return Stock::query()->where('product_id', $id)->get()->sum('stock_out'); |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | if (! function_exists('saleQTY')) { |
| 117 | function saleQTY($sale_id, $product_id) |
| 118 | { |
| 119 | return SaleDetalis::query()->where('sale_id', $sale_id)->where('product_id', $product_id)->with(['color', 'size', 'brand'])->get()->sum('qty'); |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | if (! function_exists('purchaseQTY')) { |
| 124 | function purchaseQTY($id, $product_id) |
| 125 | { |
| 126 | return PurchaseDetails::query()->where('purchase_id', $id)->where('product_id', $product_id)->with(['color', 'size', 'brand'])->get()->sum('qty'); |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | if (! function_exists('detailsInfo')) { |
| 131 | function detailsInfo($sale_id, $product_id) |
| 132 | { |
| 133 | return SaleDetalis::query()->with(['color', 'size', 'brand'])->where('sale_id', $sale_id)->where('product_id', $product_id)->get(); |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | if (! function_exists('PurchaseDetailsInfo')) { |
| 138 | function PurchaseDetailsInfo($purchase_id, $product_id) |
| 139 | { |
| 140 | return PurchaseDetails::query()->with(['color', 'size', 'brand'])->where('purchase_id', $purchase_id)->where('product_id', $product_id)->get(); |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | if (! function_exists('ColorName')) { |
| 145 | function ColorName($id) |
| 146 | { |
| 147 | return Color::findById($id); |
| 148 | |
| 149 | $color = Color::query()->findOrFail($id); |
| 150 | |
| 151 | return $color->id ?? null; |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | if (! function_exists('setting')) { |
| 156 | function setting() |
| 157 | { |
| 158 | // COMPAT SHIM (Phase 1 slice 5): the legacy single-row `settings` table |
| 159 | // was migrated into the unified store. Old call sites still do |
| 160 | // setting()->name / ->logo / ->phone1 etc., so return a lightweight |
| 161 | // object that maps those legacy property names onto settings('group.key'). |
| 162 | // New code should call settings('general.site_name') directly. |
| 163 | return new LegacySettingProxy; |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | if (! function_exists('social_setting')) { |
| 168 | function social_setting(string $key, $default = null) |
| 169 | { |
| 170 | // Cached — the actual caching lives in SocialSetting::getSetting() |
| 171 | // itself now (so every other caller of that method, e.g. the |
| 172 | // Facebook/WhatsApp/AI chat services, benefits too, not just the |
| 173 | // storefront). This helper is just a thin wrapper kept for the |
| 174 | // existing call sites. |
| 175 | return SocialSetting::getSetting($key, $default); |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | if (! function_exists('saleList')) { |
| 180 | function saleList($status) |
| 181 | { |
| 182 | $sale = Sale::query()->where('status', $status)->get(); |
| 183 | |
| 184 | return $sale; |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | if (! function_exists('deliveredSale')) { |
| 189 | function deliveredSale() |
| 190 | { |
| 191 | $sale = Sale::query()->where('created_at', today())->get(); |
| 192 | |
| 193 | return $sale; |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | if (! function_exists('colors')) { |
| 198 | function colors() |
| 199 | { |
| 200 | // Cached: admin's own color/size/brand lists, rarely change, safe to |
| 201 | // reuse across requests. No dedicated invalidation hook for these |
| 202 | // three — they use the same 6-hour TTL safety net as everything |
| 203 | // else, since they're not part of the "product/category" scope |
| 204 | // that needs instant refresh. |
| 205 | return CacheService::remember(CacheService::KEY_COLORS, function () { |
| 206 | return Color::query()->latest()->get(); |
| 207 | }); |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | if (! function_exists('sizes')) { |
| 212 | function sizes() |
| 213 | { |
| 214 | return CacheService::remember(CacheService::KEY_SIZES, function () { |
| 215 | return Size::query()->latest()->get(); |
| 216 | }); |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | if (! function_exists('brands')) { |
| 221 | function brands() |
| 222 | { |
| 223 | return CacheService::remember(CacheService::KEY_BRANDS, function () { |
| 224 | return Brand::query()->latest()->get(); |
| 225 | }); |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | if (! function_exists('menus')) { |
| 230 | function menus() |
| 231 | { |
| 232 | // Cached: rendered in the storefront header nav on EVERY page load. |
| 233 | // Invalidated immediately by Category::boot() on save/delete. |
| 234 | return CacheService::remember(CacheService::KEY_MENUS, function () { |
| 235 | return Category::with('children')->whereNull('parent_id')->get(); |
| 236 | }); |
| 237 | } |
| 238 | } |
| 239 | if (! function_exists('categories')) { |
| 240 | function categories() |
| 241 | { |
| 242 | return CacheService::remember(CacheService::KEY_CATEGORIES, function () { |
| 243 | return Category::query()->get(); |
| 244 | }); |
| 245 | } |
| 246 | } |
| 247 | if (! function_exists('products')) { |
| 248 | function products() |
| 249 | { |
| 250 | // Cached: this exact query runs TWICE on the home page alone |
| 251 | // (product-slider-section + product-section both call it), plus |
| 252 | // wherever else it's used. Invalidated immediately by |
| 253 | // Product::boot()/ProductVariation::boot() on save/delete, and by |
| 254 | // ProductController::updatePositions() (which bypasses Eloquent |
| 255 | // events with a raw mass update, so it forgets this key manually). |
| 256 | return CacheService::remember(CacheService::KEY_PRODUCTS_ALL, function () { |
| 257 | return Product::query()->with('stocks')->orderBy('position', 'ASC')->latest()->get(); |
| 258 | }); |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | if (! function_exists('discount_calculate')) { |
| 263 | function discount_calculate($price, $offer) |
| 264 | { |
| 265 | $result = $offer / 100 * $price; |
| 266 | |
| 267 | return round($price - $result); |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | if (! function_exists('colorSize')) { |
| 272 | function colorSize($product_id, $color_id) |
| 273 | { |
| 274 | return Stock::where('product_id', $product_id)->where('color_id', $color_id)->get(); |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | if (! function_exists('prev_route')) { |
| 279 | function prev_route() |
| 280 | { |
| 281 | $previousUrl = url()->previous(); |
| 282 | |
| 283 | return Route::getRoutes()->match(request()->create($previousUrl))->getName(); |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | if (! function_exists('sections')) { |
| 288 | function sections() |
| 289 | { |
| 290 | return CacheService::remember(CacheService::KEY_SECTIONS, function () { |
| 291 | return Section::query()->orderBy('position', 'ASC')->get(); |
| 292 | }); |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | if (! function_exists('order_by_status')) { |
| 297 | function order_by_status($status) |
| 298 | { |
| 299 | return Order::query()->where('status', $status)->get(); |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | if (! function_exists('payment_type')) { |
| 304 | function payment_type() |
| 305 | { |
| 306 | return PaymentType::query()->pluck('name', 'id'); |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | if (! function_exists('admin_menu_route')) { |
| 311 | function admin_menu_route($name, $params = [], $fallback = '#') |
| 312 | { |
| 313 | try { |
| 314 | return Route::has($name) ? route($name, $params) : $fallback; |
| 315 | } catch (Throwable $e) { |
| 316 | return $fallback; |
| 317 | } |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | if (! function_exists('admin_menu_is_active')) { |
| 322 | function admin_menu_is_active($url) |
| 323 | { |
| 324 | if (! $url || $url === '#') { |
| 325 | return false; |
| 326 | } |
| 327 | $a = rtrim(parse_url($url, PHP_URL_PATH) ?? '', '/'); |
| 328 | $b = rtrim(parse_url(url()->current(), PHP_URL_PATH) ?? '', '/'); |
| 329 | |
| 330 | return $a !== '' && $a === $b; |
| 331 | } |
| 332 | } |
| 333 | |
| 334 | if (! function_exists('admin_menu_group_open')) { |
| 335 | function admin_menu_group_open(array $item) |
| 336 | { |
| 337 | foreach ($item['children'] ?? [] as $child) { |
| 338 | if (admin_menu_is_active($child['url'] ?? null)) { |
| 339 | return true; |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | return false; |
| 344 | } |
| 345 | } |
| 346 | |
| 347 | if (! function_exists('storefront_theme_view')) { |
| 348 | /** |
| 349 | * Resolve an explicitly supported visual theme slot. Page content and |
| 350 | * widget data always remain in the shared website/widgets views. |
| 351 | */ |
| 352 | function storefront_theme_view(string $view, ?string $fallback = null): ?string |
| 353 | { |
| 354 | try { |
| 355 | $path = theme()->activePath(); |
| 356 | $file = $path ? $path . DIRECTORY_SEPARATOR . str_replace('.', DIRECTORY_SEPARATOR, $view) . '.blade.php' : null; |
| 357 | |
| 358 | if ($file && is_file($file)) { |
| 359 | return 'theme::' . $view; |
| 360 | } |
| 361 | } catch (Throwable $e) { |
| 362 | // Fresh install before the themes table exists. |
| 363 | } |
| 364 | |
| 365 | return $fallback ?? $view; |
| 366 | } |
| 367 | } |
| 368 | |
| 369 | if (! function_exists('admin_menu')) { |
| 370 | /** |
| 371 | * SINGLE SOURCE OF TRUTH for the admin sidebar menu. |
| 372 | * Consumed by: |
| 373 | * - resources/views/admin/layouts/include/side-nav.blade.php (every admin page) |
| 374 | * - resources/views/admin/dashboard/dashboard.blade.php (standalone CashBook shell) |
| 375 | * Add / rename / re-order entries HERE only. |
| 376 | */ |
| 377 | function admin_menu() |
| 378 | { |
| 379 | try { |
| 380 | $pendingOrders = order_by_status('Pending')->count(); |
| 381 | } catch (Throwable $e) { |
| 382 | $pendingOrders = 0; |
| 383 | } |
| 384 | |
| 385 | try { |
| 386 | $activeChats = ChatConversation::where('status', 'active')->count(); |
| 387 | } catch (Throwable $e) { |
| 388 | $activeChats = 0; |
| 389 | } |
| 390 | |
| 391 | $chatUrl = admin_menu_route('admin.chatbot.conversations'); |
| 392 | |
| 393 | $menu = [ |
| 394 | ['section' => 'MAIN', 'items' => [ |
| 395 | ['label' => 'Dashboard', 'icon' => 'fa-th-large', 'url' => url('admin/dashboard')], |
| 396 | ]], |
| 397 | ['section' => 'SALES', 'items' => [ |
| 398 | ['label' => 'POS Sale', 'icon' => 'fa-cash-register', 'children' => [ |
| 399 | ['label' => 'Sale List', 'icon' => 'fa-list', 'url' => admin_menu_route('admin.sale.index')], |
| 400 | ['label' => 'New Sale', 'icon' => 'fa-plus-circle', 'url' => admin_menu_route('admin.sale.create')], |
| 401 | ]], |
| 402 | ['label' => 'Orders', 'icon' => 'fa-shopping-bag', 'badge' => $pendingOrders, 'children' => [ |
| 403 | ['label' => 'Order List', 'icon' => 'fa-list-ul', 'url' => admin_menu_route('admin.order.index')], |
| 404 | ]], |
| 405 | ['label' => 'Coupons', 'icon' => 'fa-ticket-alt', 'feature' => 'coupons', 'url' => admin_menu_route('admin.coupons.index')], |
| 406 | // Phase 7 — wholesale/customer-group pricing. Off by default; a customer with |
| 407 | // no group assigned still pays the normal price everywhere. |
| 408 | ['label' => 'Customer Groups', 'icon' => 'fa-users-rectangle', 'feature' => 'wholesale_pricing', 'url' => admin_menu_route('admin.customer-groups.index')], |
| 409 | ['label' => 'Price Rules', 'icon' => 'fa-tags', 'feature' => 'wholesale_pricing', 'url' => admin_menu_route('admin.price-lists.index')], |
| 410 | // Phase 8 — Returns/RMA workspace. `returns` already toggles the storefront; the |
| 411 | // admin side of it (approve/reject, refund) hasn't been built yet. |
| 412 | ['label' => 'Returns & Refunds', 'icon' => 'fa-rotate-left', 'feature' => 'returns', 'soon' => true, 'url' => '#'], |
| 413 | ]], |
| 414 | ['section' => 'PURCHASING', 'feature' => 'purchase', 'items' => [ |
| 415 | ['label' => 'Purchases', 'icon' => 'fa-truck-loading', 'url' => admin_menu_route('admin.purchase.index')], |
| 416 | ['label' => 'Suppliers', 'icon' => 'fa-industry', 'url' => admin_menu_route('admin.supplier.index')], |
| 417 | ]], |
| 418 | ['section' => 'INVENTORY', 'items' => [ |
| 419 | ['label' => 'Products', 'icon' => 'fa-box-open', 'url' => admin_menu_route('admin.product.index')], |
| 420 | ['label' => 'Stock', 'icon' => 'fa-warehouse', 'children' => [ |
| 421 | ['label' => 'Product Stock', 'icon' => 'fa-cubes', 'url' => admin_menu_route('admin.product.stock')], |
| 422 | ['label' => 'Warehouses', 'icon' => 'fa-warehouse', 'feature' => 'multi_warehouse', 'url' => admin_menu_route('admin.warehouses.index')], |
| 423 | ['label' => 'Stock Transfers', 'icon' => 'fa-exchange-alt', 'feature' => 'multi_warehouse', 'url' => admin_menu_route('admin.stock-transfers.index')], |
| 424 | ]], |
| 425 | ['label' => 'Branches', 'icon' => 'fa-code-branch', 'feature' => 'branch_management', 'url' => admin_menu_route('admin.branches.index')], |
| 426 | // Phase 9 — full BOM + production, wizard-simple "Make a batch" flow. |
| 427 | ['label' => 'Manufacturing', 'icon' => 'fa-industry', 'feature' => 'manufacturing', 'children' => [ |
| 428 | ['label' => 'Bill of Materials', 'icon' => 'fa-list-check', 'url' => admin_menu_route('admin.manufacturing.boms.index')], |
| 429 | ['label' => 'Production Orders', 'icon' => 'fa-gears', 'url' => admin_menu_route('admin.manufacturing.production.index')], |
| 430 | ]], |
| 431 | ]], |
| 432 | ['section' => 'PEOPLE', 'items' => [ |
| 433 | ['label' => 'Team', 'icon' => 'fa-users', 'children' => [ |
| 434 | ['label' => 'Staff', 'icon' => 'fa-user-shield', 'url' => admin_menu_route('admin.staff.index')], |
| 435 | ['label' => 'Roles', 'icon' => 'fa-user-lock', 'feature' => 'rbac', 'url' => admin_menu_route('admin.roles.index')], |
| 436 | ['label' => 'Activity Log', 'icon' => 'fa-history', 'feature' => 'audit_log', 'can' => 'view_audit_log', 'url' => admin_menu_route('admin.audit.index')], |
| 437 | ]], |
| 438 | ['label' => 'Employees', 'icon' => 'fa-id-badge', 'children' => [ |
| 439 | ['label' => 'Employee List', 'icon' => 'fa-list', 'url' => admin_menu_route('admin.employee.index')], |
| 440 | ['label' => 'Add Employee', 'icon' => 'fa-plus-circle', 'url' => admin_menu_route('admin.employee.create')], |
| 441 | ]], |
| 442 | ['label' => 'Customers', 'icon' => 'fa-user-tag', 'url' => admin_menu_route('admin.customer.index')], |
| 443 | ['label' => 'Reviews', 'icon' => 'fa-star', 'feature' => 'reviews', 'url' => admin_menu_route('admin.customer-reviews.index')], |
| 444 | ]], |
| 445 | // Phase 12 — vendors each own their products/stock/orders; commission + payouts. |
| 446 | ['section' => 'MARKETPLACE', 'feature' => 'multi_vendor', 'items' => [ |
| 447 | ['label' => 'Vendors', 'icon' => 'fa-store', 'url' => admin_menu_route('admin.vendors.index')], |
| 448 | ['label' => 'Commissions', 'icon' => 'fa-percent', 'url' => admin_menu_route('admin.commissions.index')], |
| 449 | ['label' => 'Payouts', 'icon' => 'fa-hand-holding-dollar', 'feature' => 'vendor_payouts', 'url' => admin_menu_route('admin.payouts.index')], |
| 450 | ]], |
| 451 | // Phase 9.5 — the "ACCOUNTS" section from the design mockup: income, expense, |
| 452 | // CashBook / Bank Book, non-accountant friendly. |
| 453 | ['section' => 'ACCOUNTING', 'feature' => 'accounting', 'items' => [ |
| 454 | ['label' => 'Income', 'icon' => 'fa-arrow-circle-up', 'url' => admin_menu_route('admin.transactions.income.index')], |
| 455 | ['label' => 'Expense', 'icon' => 'fa-arrow-circle-down', 'url' => admin_menu_route('admin.transactions.expense.index')], |
| 456 | ['label' => 'CashBook', 'icon' => 'fa-money-bill-wave', 'url' => admin_menu_route('admin.cashbook.index')], |
| 457 | ['label' => 'Bank Book', 'icon' => 'fa-building-columns', 'feature' => 'bank_book', 'url' => admin_menu_route('admin.bankbook.index')], |
| 458 | ['label' => 'Accounts', 'icon' => 'fa-list-ul', 'children' => [ |
| 459 | ['label' => 'Accounts', 'icon' => 'fa-wallet', 'url' => admin_menu_route('admin.accounts.index')], |
| 460 | ['label' => 'Categories', 'icon' => 'fa-tags', 'url' => admin_menu_route('admin.account-categories.index')], |
| 461 | ]], |
| 462 | ]], |
| 463 | // Phase 10 — full reporting engine (Excel/PDF/Print on every report, |
| 464 | // grouped Sales/Purchase/Inventory/Finance/Manufacturing/Customers). |
| 465 | // config/reports.php is the registry; each entry hides itself if its |
| 466 | // feature is off (see App\Services\Reporting\ReportService::visible()). |
| 467 | // The legacy Sale-based report pages (admin.xyz / report.purchases / |
| 468 | // report.customer / report.supplier) still exist — still linked from |
| 469 | // a couple of POS due-payment screens — but are no longer in this menu. |
| 470 | ['section' => 'REPORTS', 'items' => [ |
| 471 | ['label' => 'All Reports', 'icon' => 'fa-chart-bar', 'url' => admin_menu_route('admin.reports.index')], |
| 472 | ['label' => 'Sales Summary', 'icon' => 'fa-chart-line', 'url' => admin_menu_route('admin.reports.show', ['key' => 'sales-summary'])], |
| 473 | ['label' => 'Purchase Summary', 'icon' => 'fa-truck-loading', 'url' => admin_menu_route('admin.reports.show', ['key' => 'purchase-summary'])], |
| 474 | ['label' => 'Current Stock', 'icon' => 'fa-warehouse', 'url' => admin_menu_route('admin.reports.show', ['key' => 'current-stock'])], |
| 475 | ['label' => 'Customers', 'icon' => 'fa-user-tag', 'url' => admin_menu_route('admin.reports.show', ['key' => 'customer-list'])], |
| 476 | ['label' => 'Finance (P&L)', 'icon' => 'fa-hand-holding-usd', 'feature' => 'accounting', 'url' => admin_menu_route('admin.reports.show', ['key' => 'income-expense'])], |
| 477 | ['label' => 'Production Output', 'icon' => 'fa-industry', 'feature' => 'manufacturing', 'url' => admin_menu_route('admin.reports.show', ['key' => 'production-output'])], |
| 478 | ]], |
| 479 | ['section' => 'CHAT', 'feature' => 'chatbot', 'items' => [ |
| 480 | ['label' => 'Chat', 'icon' => 'fa-comments', 'badge' => $activeChats, 'children' => [ |
| 481 | ['label' => 'All Chats', 'icon' => 'fa-inbox', 'url' => $chatUrl], |
| 482 | ['label' => 'Messenger', 'icon' => 'fa-facebook-messenger', 'fab' => true, 'url' => $chatUrl !== '#' ? $chatUrl . '?platform=facebook' : '#'], |
| 483 | ['label' => 'WhatsApp', 'icon' => 'fa-whatsapp', 'fab' => true, 'url' => $chatUrl !== '#' ? $chatUrl . '?platform=whatsapp' : '#'], |
| 484 | ]], |
| 485 | ]], |
| 486 | ['section' => 'SETTINGS', 'items' => [ |
| 487 | ['label' => 'Ecommerce Setup', 'icon' => 'fa-store', 'children' => [ |
| 488 | ['label' => 'Themes', 'icon' => 'fa-palette', 'url' => admin_menu_route('admin.themes.index')], |
| 489 | ['label' => 'Page Builder', 'icon' => 'fa-magic', 'url' => admin_menu_route('admin.widgets.area', ['area' => 'home_main'])], |
| 490 | ['label' => 'Pages', 'icon' => 'fa-file-alt', 'url' => admin_menu_route('admin.pages.index')], |
| 491 | ['label' => 'Collections', 'icon' => 'fa-layer-group', 'url' => admin_menu_route('admin.collections.index')], |
| 492 | ['label' => 'Navigation Menus', 'icon' => 'fa-bars', 'url' => admin_menu_route('admin.menus.index')], |
| 493 | ['label' => 'Widgets', 'icon' => 'fa-puzzle-piece', 'url' => admin_menu_route('admin.widgets.index')], |
| 494 | ['label' => 'Sliders', 'icon' => 'fa-images', 'url' => admin_menu_route('admin.sliders.index')], |
| 495 | ['label' => 'Media Library', 'icon' => 'fa-photo-video', 'url' => admin_menu_route('admin.media.index')], |
| 496 | ['label' => 'Categories', 'icon' => 'fa-tags', 'url' => admin_menu_route('admin.category.index')], |
| 497 | ['label' => 'Brands', 'icon' => 'fa-trademark', 'url' => admin_menu_route('admin.brand.index')], |
| 498 | ['label' => 'Attributes', 'icon' => 'fa-list-alt', 'url' => admin_menu_route('admin.attribute.index')], |
| 499 | ['label' => 'Colors', 'icon' => 'fa-palette', 'url' => admin_menu_route('admin.color.index')], |
| 500 | ['label' => 'Sizes', 'icon' => 'fa-ruler', 'url' => admin_menu_route('admin.size.index')], |
| 501 | ['label' => 'Product Origins', 'icon' => 'fa-globe', 'url' => admin_menu_route('admin.origin.index')], |
| 502 | ['label' => 'Social Media', 'icon' => 'fa-share-alt', 'url' => admin_menu_route('admin.social.media')], |
| 503 | ['label' => 'Shipping Zones', 'icon' => 'fa-truck-fast', 'url' => admin_menu_route('admin.shipping-zones.index')], |
| 504 | ['label' => 'Tax by Category', 'icon' => 'fa-percent', 'feature' => 'tax_vat', 'url' => admin_menu_route('admin.category-tax.index')], |
| 505 | ['label' => 'Payment Gateways', 'icon' => 'fa-credit-card', 'url' => admin_menu_route('admin.payment-gateways.index')], |
| 506 | ['label' => 'Notifications', 'icon' => 'fa-bell', 'url' => admin_menu_route('admin.notification-settings.index')], |
| 507 | ]], |
| 508 | ['label' => 'Configuration', 'icon' => 'fa-cog', 'children' => [ |
| 509 | ['label' => 'Setup Guide', 'icon' => 'fa-magic', 'url' => admin_menu_route('admin.setup.index')], |
| 510 | ['label' => 'Settings', 'icon' => 'fa-sliders-h', 'url' => admin_menu_route('admin.settings.index')], |
| 511 | ['label' => 'Help Texts', 'icon' => 'fa-question-circle', 'url' => admin_menu_route('admin.help.index')], |
| 512 | ['label' => 'Features', 'icon' => 'fa-toggle-on', 'url' => admin_menu_route('admin.features.edit')], |
| 513 | ['label' => 'Payment Types', 'icon' => 'fa-credit-card', 'url' => admin_menu_route('admin.paymentType.index')], |
| 514 | ]], |
| 515 | // Phase 8 — every gateway (COD, SSLCommerz, bKash, Nagad, Rocket, Upay, |
| 516 | // Stripe, PayPal) pre-registered; operator just pastes API keys. COD is |
| 517 | // always on; SSLCommerz/bKash/Nagad/Rocket/Upay depend_on `online_payments`, |
| 518 | // Stripe/PayPal on `international_payments` — see config/payment_gateways.php. |
| 519 | // Phase 8 — email/SMS/WhatsApp per-event matrix with templates. |
| 520 | // Phase 8 — Email/SMS/WhatsApp channels + the event x channel matrix. |
| 521 | // Sends nothing until a channel is configured AND an event opts into it. |
| 522 | // Phase 11 — picks shared-cPanel vs VPS in Settings → Deployment |
| 523 | // (config/settings/deployment.php), this page shows the matching |
| 524 | // cron/.env/Supervisor instructions. Display-only, never edits |
| 525 | // server files itself. |
| 526 | ['label' => 'Deployment Guide', 'icon' => 'fa-server', 'url' => admin_menu_route('admin.deployment.guide')], |
| 527 | // Phase 11 — database backup list, manual trigger, and download. |
| 528 | // Requires the scheduler to be running for automatic nightly backups; |
| 529 | // the Deployment Guide explains the cron setup. |
| 530 | ['label' => 'Backups', 'icon' => 'fa-database', 'url' => admin_menu_route('admin.backups.index'), 'can' => 'manage_settings'], |
| 531 | // Phase 11 Slice 7 — mint/revoke Sanctum tokens for the /api/v1 surface. |
| 532 | ['label' => 'API Tokens', 'icon' => 'fa-key', 'url' => admin_menu_route('admin.api-tokens.index'), 'can' => 'manage_settings'], |
| 533 | // Phase 13 — license activation + one-click updater. |
| 534 | ['label' => 'License & Updates', 'icon' => 'fa-key', 'soon' => true, 'url' => '#'], |
| 535 | ]], |
| 536 | ]; |
| 537 | |
| 538 | $processed = admin_menu_apply_permissions(admin_menu_apply_features($menu)); |
| 539 | |
| 540 | foreach ($processed as &$section) { |
| 541 | if (! isset($section['items'])) { |
| 542 | continue; |
| 543 | } |
| 544 | foreach ($section['items'] as &$item) { |
| 545 | if (isset($item['children']) && count($item['children']) === 1) { |
| 546 | $child = reset($item['children']); |
| 547 | $item['url'] = $child['url'] ?? '#'; |
| 548 | $item['soon'] = ! empty($child['soon']); |
| 549 | unset($item['children']); |
| 550 | } |
| 551 | } |
| 552 | } |
| 553 | |
| 554 | return $processed; |
| 555 | } |
| 556 | } |
| 557 | |
| 558 | if (! function_exists('admin_menu_apply_permissions')) { |
| 559 | /** |
| 560 | * Hide menu entries the current staff role can't reach. Uses each item's |
| 561 | * `url` (route names mirror paths in this app). rbac off -> no-op. |
| 562 | */ |
| 563 | function admin_menu_apply_permissions(array $menu): array |
| 564 | { |
| 565 | $perm = app(PermissionService::class); |
| 566 | if (! $perm->enforced()) { |
| 567 | return $menu; |
| 568 | } |
| 569 | |
| 570 | $keep = function ($node) use ($perm) { |
| 571 | $ability = ! empty($node['can']) |
| 572 | ? $node['can'] |
| 573 | : $perm->abilityForPath($node['url'] ?? null); |
| 574 | |
| 575 | return ! $ability || $perm->allows($ability); |
| 576 | }; |
| 577 | |
| 578 | $sections = []; |
| 579 | foreach ($menu as $section) { |
| 580 | $items = []; |
| 581 | foreach ($section['items'] ?? [] as $item) { |
| 582 | if (! empty($item['children'])) { |
| 583 | $item['children'] = array_values(array_filter($item['children'], $keep)); |
| 584 | if ($item['children'] === []) { |
| 585 | continue; |
| 586 | } |
| 587 | } elseif (! $keep($item)) { |
| 588 | continue; |
| 589 | } |
| 590 | $items[] = $item; |
| 591 | } |
| 592 | if ($items !== []) { |
| 593 | $section['items'] = $items; |
| 594 | $sections[] = $section; |
| 595 | } |
| 596 | } |
| 597 | |
| 598 | return $sections; |
| 599 | } |
| 600 | } |
| 601 | |
| 602 | if (! function_exists('admin_menu_apply_features')) { |
| 603 | /** |
| 604 | * Strip menu sections / items / children whose `feature` flag is off. |
| 605 | * A group left with no children after filtering is dropped too. |
| 606 | */ |
| 607 | function admin_menu_apply_features(array $menu): array |
| 608 | { |
| 609 | $feature = app(FeatureService::class); |
| 610 | |
| 611 | $keep = fn ($node) => empty($node['feature']) || $feature->enabled($node['feature']); |
| 612 | |
| 613 | $sections = []; |
| 614 | foreach ($menu as $section) { |
| 615 | if (! $keep($section)) { |
| 616 | continue; |
| 617 | } |
| 618 | |
| 619 | $items = []; |
| 620 | foreach ($section['items'] ?? [] as $item) { |
| 621 | if (! $keep($item)) { |
| 622 | continue; |
| 623 | } |
| 624 | |
| 625 | if (! empty($item['children'])) { |
| 626 | $item['children'] = array_values(array_filter($item['children'], $keep)); |
| 627 | if ($item['children'] === []) { |
| 628 | continue; |
| 629 | } |
| 630 | } |
| 631 | |
| 632 | $items[] = $item; |
| 633 | } |
| 634 | |
| 635 | if ($items !== []) { |
| 636 | $section['items'] = $items; |
| 637 | $sections[] = $section; |
| 638 | } |
| 639 | } |
| 640 | |
| 641 | return $sections; |
| 642 | } |
| 643 | } |
| 644 | |
| 645 | // if(!function_exists('PurchasePayInfo')){ |
| 646 | // function PurchaseCardPayTotal($id,$product_id) |
| 647 | // { |
| 648 | // return \App\Models\PurchaseCard::query()->where('supplier_id',); |
| 649 | // } |
| 650 | // } |