Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.48% covered (warning)
71.48%
203 / 284
36.59% covered (danger)
36.59%
15 / 41
CRAP
n/a
0 / 0
CurrencyFormate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
moneyIcon
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
addIconSVG
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
totalPurchase
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
totalSale
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
totalPurchaseDue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
totalSaleDue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
totalCustomer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
totalSupplier
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
totalSaleInvoice
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
totalPurchaseInvoice
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
stockInSum
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
stockOutSum
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
saleQTY
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
purchaseQTY
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
detailsInfo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
PurchaseDetailsInfo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
ColorName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
setting
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
social_setting
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
saleList
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
deliveredSale
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
colors
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
sizes
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
brands
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
menus
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
categories
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
products
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
discount_calculate
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
colorSize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
prev_route
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
sections
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
order_by_status
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
payment_type
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
admin_menu_route
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
5.67
admin_menu_is_active
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
admin_menu_group_open
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
storefront_theme_view
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
5.12
admin_menu
96.18% covered (success)
96.18%
126 / 131
0.00% covered (danger)
0.00%
0 / 1
10
admin_menu_apply_permissions
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
10
admin_menu_apply_features
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
9.01
1<?php
2
3use App\Models\admin\Customer;
4use App\Models\admin\Product;
5use App\Models\admin\Purchase;
6use App\Models\admin\PurchaseDetails;
7use App\Models\admin\PurchasePayment;
8use App\Models\admin\Sale;
9use App\Models\admin\SaleDetalis;
10use App\Models\admin\SalePayment;
11use App\Models\admin\Stock;
12use App\Models\admin\Supplier;
13use App\Models\Brand;
14use App\Models\Category;
15use App\Models\ChatConversation;
16use App\Models\Color;
17use App\Models\Order;
18use App\Models\PaymentType;
19use App\Models\Section;
20use App\Models\Setting;
21use App\Models\Size;
22use App\Models\SocialSetting;
23use App\Service\CacheService;
24use App\Services\Feature\FeatureService;
25use App\Services\Permission\PermissionService;
26use App\Support\LegacySettingProxy;
27use Illuminate\Support\Facades\Route;
28
29function CurrencyFormate($value)
30{
31    return moneyIcon() . number_format($value, 2);
32}
33
34if (! function_exists('moneyIcon')) {
35    function moneyIcon()
36    {
37        return settings('general.currency_symbol', '৳') . ' ';
38    }
39}
40
41if (! 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
51if (! function_exists('totalPurchase')) {
52    function totalPurchase()
53    {
54
55        return PurchasePayment::query()->get()->sum('total');
56    }
57}
58
59if (! function_exists('totalSale')) {
60    function totalSale()
61    {
62        return SalePayment::query()->get()->sum('total');
63    }
64}
65if (! function_exists('totalPurchaseDue')) {
66    function totalPurchaseDue()
67    {
68        return PurchasePayment::query()->get()->sum('due');
69    }
70}
71
72if (! function_exists('totalSaleDue')) {
73    function totalSaleDue()
74    {
75        return SalePayment::query()->get()->sum('due');
76    }
77}
78if (! function_exists('totalCustomer')) {
79    function totalCustomer()
80    {
81        return Customer::query()->get()->count();
82    }
83}
84if (! function_exists('totalSupplier')) {
85    function totalSupplier()
86    {
87        return Supplier::query()->get()->count();
88    }
89}
90if (! function_exists('totalSaleInvoice')) {
91    function totalSaleInvoice()
92    {
93        return Sale::query()->get()->count();
94    }
95}
96if (! function_exists('totalPurchaseInvoice')) {
97    function totalPurchaseInvoice()
98    {
99        return Purchase::query()->get()->count();
100    }
101}
102if (! function_exists('stockInSum')) {
103    function stockInSum($id)
104    {
105        return Stock::query()->where('product_id', $id)->get()->sum('stock_in');
106    }
107}
108
109if (! function_exists('stockOutSum')) {
110    function stockOutSum($id)
111    {
112        return Stock::query()->where('product_id', $id)->get()->sum('stock_out');
113    }
114}
115
116if (! 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
123if (! 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
130if (! 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
137if (! 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
144if (! 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
155if (! 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
167if (! 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
179if (! function_exists('saleList')) {
180    function saleList($status)
181    {
182        $sale = Sale::query()->where('status', $status)->get();
183
184        return $sale;
185    }
186}
187
188if (! function_exists('deliveredSale')) {
189    function deliveredSale()
190    {
191        $sale = Sale::query()->where('created_at', today())->get();
192
193        return $sale;
194    }
195}
196
197if (! 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
211if (! 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
220if (! 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
229if (! 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}
239if (! function_exists('categories')) {
240    function categories()
241    {
242        return CacheService::remember(CacheService::KEY_CATEGORIES, function () {
243            return Category::query()->get();
244        });
245    }
246}
247if (! 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
262if (! 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
271if (! 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
278if (! 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
287if (! 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
296if (! function_exists('order_by_status')) {
297    function order_by_status($status)
298    {
299        return Order::query()->where('status', $status)->get();
300    }
301}
302
303if (! function_exists('payment_type')) {
304    function payment_type()
305    {
306        return PaymentType::query()->pluck('name', 'id');
307    }
308}
309
310if (! 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
321if (! 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
334if (! 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
347if (! 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
369if (! 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
558if (! 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
602if (! 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// }