diff --git a/app/Enums/EmployeeAdvanceStatus.php b/app/Enums/EmployeeAdvanceStatus.php index 2233737..8d8f29b 100644 --- a/app/Enums/EmployeeAdvanceStatus.php +++ b/app/Enums/EmployeeAdvanceStatus.php @@ -11,6 +11,7 @@ enum EmployeeAdvanceStatus: string case PENDING = 'pending'; case APPROVED = 'approved'; case REJECTED = 'rejected'; + case PARTIALLY_PAID = 'partially_paid'; case PAID = 'paid'; public function label(): string @@ -19,6 +20,7 @@ public function label(): string self::PENDING => 'Menunggu', self::APPROVED => 'Disetujui', self::REJECTED => 'Ditolak', + self::PARTIALLY_PAID => 'Dibayar Sebagian', self::PAID => 'Lunas', }; } diff --git a/app/Enums/Role.php b/app/Enums/Role.php index b26bd39..fb7f1b9 100644 --- a/app/Enums/Role.php +++ b/app/Enums/Role.php @@ -13,7 +13,8 @@ enum Role: string case ADMIN_TOKO = 'admin-toko'; case ADMIN_BAHAN_BAKU = 'admin-bahan-baku'; case DIREKTUR = 'direktur'; - case MARKETING = 'marketing'; + case MARKETING_OFFLINE = 'marketing-offline'; + case MARKETING_ONLINE = 'marketing-online'; case CASHIER = 'cashier'; case NON_OPERATOR = 'non-operator'; @@ -25,7 +26,8 @@ public function label(): string self::ADMIN_TOKO => 'Admin Toko', self::ADMIN_BAHAN_BAKU => 'Admin Bahan Baku', self::DIREKTUR => 'Direktur', - self::MARKETING => 'Marketing', + self::MARKETING_OFFLINE => 'Marketing Offline', + self::MARKETING_ONLINE => 'Marketing Online', self::CASHIER => 'Kasir', self::NON_OPERATOR => 'Non Operator', }; @@ -63,7 +65,6 @@ public function permissions(): array Permission::ANALYSIS_ATTENDANCE, Permission::ANALYSIS_CASH, - Permission::ANALYSIS_RAW_MATERIALS, Permission::ANALYSIS_PRODUCT_STOCK, Permission::ANALYSIS_REVENUE, Permission::ANALYSIS_EXPENSE, @@ -72,14 +73,11 @@ public function permissions(): array Permission::ANALYSIS_PROFIT_HPP, Permission::ANALYSIS_PROFIT_GROSS, Permission::ANALYSIS_PROFIT_MARGIN, - Permission::ANALYSIS_TOP_SUPPLIERS, Permission::ANALYSIS_TOP_CUSTOMERS, Permission::ANALYSIS_TOP_PRODUCTS, Permission::EMPLOYEES_VIEW, - Permission::STOCKS_VIEW, - Permission::ATTENDANCES_VIEW, Permission::ATTENDANCES_CREATE, Permission::ATTENDANCES_DELETE, @@ -92,16 +90,10 @@ public function permissions(): array Permission::CATEGORIES_VIEW, - Permission::SUPPLIERS_VIEW, - Permission::CUSTOMERS_VIEW, Permission::PRODUCTS_VIEW, - Permission::RAW_MATERIALS_VIEW, - - Permission::PURCHASES_VIEW, - Permission::ORDERS_VIEW, Permission::CUTTINGS_VIEW, @@ -203,7 +195,6 @@ public function permissions(): array self::CASHIER => [ Permission::DASHBOARD_VIEW, Permission::DASHBOARD_ATTENDANCE, - Permission::DASHBOARD_CASH, Permission::DASHBOARD_REVENUE, Permission::DASHBOARD_ORDERS_CHANNEL, Permission::DASHBOARD_ORDERS_PAYMENT, @@ -211,13 +202,8 @@ public function permissions(): array Permission::ANALYSIS_VIEW, Permission::ANALYSIS_ATTENDANCE, - Permission::ANALYSIS_CASH, Permission::ANALYSIS_REVENUE, - Permission::ANALYSIS_EXPENSE, Permission::ANALYSIS_PROFIT_ORDERS, - Permission::ANALYSIS_PROFIT_HPP, - Permission::ANALYSIS_PROFIT_GROSS, - Permission::ANALYSIS_PROFIT_MARGIN, Permission::ANALYSIS_TOP_CUSTOMERS, Permission::ANALYSIS_TOP_PRODUCTS, @@ -303,7 +289,7 @@ public function permissions(): array Permission::PAYROLL_VIEW, ], - self::MARKETING => [ + self::MARKETING_OFFLINE, self::MARKETING_ONLINE => [ Permission::DASHBOARD_VIEW, Permission::DASHBOARD_ATTENDANCE, Permission::DASHBOARD_REVENUE, @@ -316,10 +302,6 @@ public function permissions(): array Permission::ANALYSIS_ATTENDANCE, Permission::ANALYSIS_REVENUE, Permission::ANALYSIS_PROFIT_ORDERS, - Permission::ANALYSIS_PROFIT_HPP, - Permission::ANALYSIS_PROFIT_GROSS, - Permission::ANALYSIS_PROFIT_MARGIN, - Permission::ANALYSIS_TOP_CUSTOMERS, Permission::ANALYSIS_TOP_PRODUCTS, Permission::ATTENDANCES_VIEW, @@ -330,8 +312,6 @@ public function permissions(): array Permission::LEAVE_REQUESTS_UPDATE, Permission::LEAVE_REQUESTS_DELETE, - Permission::CUSTOMERS_VIEW, - Permission::PRODUCTS_VIEW, Permission::ORDERS_VIEW, diff --git a/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php b/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php index d4951d0..3b5d709 100644 --- a/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php +++ b/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php @@ -91,11 +91,13 @@ public function reject(RejectEmployeeAdvanceRequest $request, EmployeeAdvance $e return redirect()->route('admin.finance.employee_advances.index'); } - public function pay(EmployeeAdvance $employeeAdvance): RedirectResponse + public function pay(Request $request, EmployeeAdvance $employeeAdvance): RedirectResponse { - $this->employeeAdvanceService->pay($employeeAdvance, auth()->user()); + $payAmount = $request->input('amount') ? (int) $request->input('amount') : null; - $this->flashSuccess('Kasbon berhasil dilunasi.'); + $this->employeeAdvanceService->pay($employeeAdvance, auth()->user(), $payAmount); + + $this->flashSuccess('Kasbon berhasil dibayar.'); return redirect()->route('admin.finance.employee_advances.index'); } diff --git a/app/Http/Requests/Admin/Manage/OrderRequest.php b/app/Http/Requests/Admin/Manage/OrderRequest.php index 9095ca0..d19f987 100644 --- a/app/Http/Requests/Admin/Manage/OrderRequest.php +++ b/app/Http/Requests/Admin/Manage/OrderRequest.php @@ -38,7 +38,6 @@ public function rules(): array 'tiktok_order_id' => [Rule::requiredIf(fn () => $this->channel === OrderChannel::TIKTOK->value), 'string', 'max:100'], 'shopee_order_id' => [Rule::requiredIf(fn () => $this->channel === OrderChannel::SHOPEE->value), 'string', 'max:100'], 'discount' => ['nullable', 'integer', 'min:0'], - 'shipping_cost' => ['nullable', 'integer', 'min:0'], 'notes' => ['nullable', 'string'], ]; @@ -71,7 +70,6 @@ public function attributes(): array 'tiktok_order_id' => 'ID pesanan TikTok Shop', 'shopee_order_id' => 'ID pesanan Shopee', 'discount' => 'diskon', - 'shipping_cost' => 'ongkos kirim', 'notes' => 'keterangan', 'items' => 'produk', 'items.*.product_variant_id' => 'varian produk', @@ -82,16 +80,18 @@ public function attributes(): array public function withValidator(Validator $validator): void { - if (! $this->isMethod('PUT') && ! $this->isMethod('PATCH')) { - return; - } - $validator->after(function (Validator $validator): void { - /** @var Order $order */ - $order = $this->route('order'); + if ($this->filled('marketing_id') && $this->marketing_id !== 'none' && ! $this->filled('customer_id')) { + $validator->errors()->add('customer_id', 'Pelanggan wajib diisi jika marketing dipilih.'); + } - if (! $order->status->isEditable()) { - $validator->errors()->add('status', 'Pesanan tidak dapat diubah.'); + if ($this->isMethod('PUT') || $this->isMethod('PATCH')) { + /** @var Order $order */ + $order = $this->route('order'); + + if (! $order->status->isEditable()) { + $validator->errors()->add('status', 'Pesanan tidak dapat diubah.'); + } } }); } diff --git a/app/Models/EmployeeAdvance.php b/app/Models/EmployeeAdvance.php index 5d5809b..a011f3f 100644 --- a/app/Models/EmployeeAdvance.php +++ b/app/Models/EmployeeAdvance.php @@ -17,6 +17,9 @@ #[Guarded(['id'])] #[Appends([ 'amount_formatted', + 'paid_amount_formatted', + 'remaining_amount', + 'remaining_amount_formatted', 'due_date_formatted', 'due_date_input', 'status_label', @@ -35,6 +38,7 @@ protected function casts(): array { return [ 'amount' => 'integer', + 'paid_amount' => 'integer', 'due_date' => 'date', 'status' => EmployeeAdvanceStatus::class, 'verified_at' => 'datetime', @@ -74,10 +78,31 @@ public function amountFormatted(): Attribute ); } + public function paidAmountFormatted(): Attribute + { + return Attribute::make( + get: fn () => 'Rp '.number_format($this->paid_amount, 0, ',', '.'), + ); + } + + public function remainingAmount(): Attribute + { + return Attribute::make( + get: fn () => $this->amount - $this->paid_amount, + ); + } + + public function remainingAmountFormatted(): Attribute + { + return Attribute::make( + get: fn () => 'Rp '.number_format($this->remaining_amount, 0, ',', '.'), + ); + } + public function canPay(): Attribute { return Attribute::make( - get: fn () => $this->status === EmployeeAdvanceStatus::APPROVED, + get: fn () => in_array($this->status, [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID], true), ); } diff --git a/app/Models/Order.php b/app/Models/Order.php index e8e3824..27eb30a 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -22,7 +22,6 @@ #[Appends([ 'subtotal_formatted', 'discount_formatted', - 'shipping_cost_formatted', 'total_amount_formatted', 'created_at_formatted', 'channel_label', @@ -46,7 +45,6 @@ protected function casts(): array 'status' => OrderStatus::class, 'subtotal' => 'integer', 'discount' => 'integer', - 'shipping_cost' => 'integer', 'marketplace_settings_snapshot' => 'array', 'total_amount' => 'integer', ]; @@ -98,13 +96,6 @@ public function discountFormatted(): Attribute ); } - public function shippingCostFormatted(): Attribute - { - return Attribute::make( - get: fn () => 'Rp '.number_format($this->shipping_cost, 0, ',', '.'), - ); - } - public function totalAmountFormatted(): Attribute { return Attribute::make( diff --git a/app/Models/Payroll.php b/app/Models/Payroll.php index 4a58ddf..91fc447 100644 --- a/app/Models/Payroll.php +++ b/app/Models/Payroll.php @@ -129,8 +129,9 @@ public function calculateKasbonDeduction(): int { $outstanding = (int) EmployeeAdvance::query() ->where('employee_id', $this->employee_id) - ->where('status', EmployeeAdvanceStatus::APPROVED) - ->sum('amount'); + ->whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID]) + ->selectRaw('SUM(amount - paid_amount) as remaining') + ->value('remaining'); return min($outstanding, (int) $this->base_salary + (int) $this->adjustments() ->where('type', PayrollAdjustmentType::BONUS) diff --git a/app/Services/Finance/EmployeeAdvanceService.php b/app/Services/Finance/EmployeeAdvanceService.php index f375c2b..a73d0da 100644 --- a/app/Services/Finance/EmployeeAdvanceService.php +++ b/app/Services/Finance/EmployeeAdvanceService.php @@ -28,10 +28,11 @@ public function __construct( */ public function outstandingSummary(?User $user = null): array { - $query = EmployeeAdvance::query()->approved() + $query = EmployeeAdvance::query() + ->whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID]) ->when(! $user->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]), fn (Builder $query) => $query->where('employee_id', $user->employee?->id ?? -1)); - $outstandingAmount = (int) $query->sum('amount'); + $outstandingAmount = (int) $query->selectRaw('SUM(amount - paid_amount) as remaining')->value('remaining'); $outstandingCount = $query->count(); @@ -242,29 +243,37 @@ public function reject(EmployeeAdvance $employeeAdvance, string $reason, User $u } } - public function pay(EmployeeAdvance $employeeAdvance, User $user): void + public function pay(EmployeeAdvance $employeeAdvance, User $user, ?int $payAmount = null): void { try { - DB::transaction(function () use ($employeeAdvance, $user): void { + DB::transaction(function () use ($employeeAdvance, $user, $payAmount): void { $employeeAdvance->loadMissing('employee.user.profile'); + $remaining = $employeeAdvance->amount - $employeeAdvance->paid_amount; + $amountToPay = $payAmount !== null ? min($payAmount, $remaining) : $remaining; + $isFullPayment = ($employeeAdvance->paid_amount + $amountToPay) >= $employeeAdvance->amount; + $description = sprintf( - 'Pelunasan kasbon: %s', + '%s kasbon: %s', + $isFullPayment ? 'Pelunasan' : 'Pembayaran sebagian kasbon', $employeeAdvance->employeeName, ); $cashTransaction = $this->cashService->recordIncoming( $employeeAdvance, - $employeeAdvance->amount, + $amountToPay, $description, $user, ); + $newPaidAmount = $employeeAdvance->paid_amount + $amountToPay; + $employeeAdvance->update([ - 'repayment_cash_transaction_id' => $cashTransaction->id, - 'paid_at' => now(), - 'paid_by_id' => $user->id, - 'status' => EmployeeAdvanceStatus::PAID, + 'paid_amount' => $newPaidAmount, + 'repayment_cash_transaction_id' => $isFullPayment ? $cashTransaction->id : $employeeAdvance->repayment_cash_transaction_id, + 'paid_at' => $isFullPayment ? now() : $employeeAdvance->paid_at, + 'paid_by_id' => $isFullPayment ? $user->id : $employeeAdvance->paid_by_id, + 'status' => $isFullPayment ? EmployeeAdvanceStatus::PAID : EmployeeAdvanceStatus::PARTIALLY_PAID, ]); }); } catch (ValidationException $e) { diff --git a/app/Services/Finance/PayrollService.php b/app/Services/Finance/PayrollService.php index 8d76015..ed27925 100644 --- a/app/Services/Finance/PayrollService.php +++ b/app/Services/Finance/PayrollService.php @@ -437,7 +437,7 @@ private function settleKasbonFromPayroll(Payroll $payroll, User $user): void $advances = EmployeeAdvance::query() ->where('employee_id', $payroll->employee_id) - ->where('status', EmployeeAdvanceStatus::APPROVED) + ->whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID]) ->orderBy('created_at') ->get(); @@ -446,12 +446,21 @@ private function settleKasbonFromPayroll(Payroll $payroll, User $user): void break; } - $advance->paid_at = now(); - $advance->paid_by_id = $user->id; - $advance->status = EmployeeAdvanceStatus::PAID; + $advanceRemaining = $advance->amount - $advance->paid_amount; + $paymentAmount = min($remaining, $advanceRemaining); + $advance->paid_amount += $paymentAmount; + + if ($advance->paid_amount >= $advance->amount) { + $advance->paid_at = now(); + $advance->paid_by_id = $user->id; + $advance->status = EmployeeAdvanceStatus::PAID; + } else { + $advance->status = EmployeeAdvanceStatus::PARTIALLY_PAID; + } + $advance->save(); - $remaining -= $advance->amount; + $remaining -= $paymentAmount; } } diff --git a/app/Services/Manage/OrderService.php b/app/Services/Manage/OrderService.php index 35b16b5..5cab355 100644 --- a/app/Services/Manage/OrderService.php +++ b/app/Services/Manage/OrderService.php @@ -87,7 +87,8 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi 'items.productVariant.product:id,name', 'items.productVariant:id,product_id,name', ]) - ->when($user->hasRole('marketing'), fn (Builder $query) => $query->where('marketing_id', $user->id)) + ->when($user->hasAnyRole(['marketing-offline', 'marketing-online']), fn (Builder $query) => $query->where('marketing_id', $user->id)) + ->when($user->hasRole('cashier'), fn (Builder $query) => $query->where('created_by_id', $user->id)) ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void { $search = $tableQuery['search']; $query->where(function (Builder $query) use ($search): void { @@ -143,7 +144,7 @@ public function marketingOptions(): array { return User::query() ->active() - ->whereHas('roles', fn (Builder $roleQuery) => $roleQuery->where('name', 'marketing')) + ->whereHas('roles', fn (Builder $roleQuery) => $roleQuery->whereIn('name', ['marketing-offline', 'marketing-online'])) ->with('profile:user_id,full_name') ->orderBy('username') ->get(['id', 'username']) @@ -417,8 +418,7 @@ public function create(array $validated, User $user): Order $subtotal = $draftItems->sum('subtotal'); $discount = (int) ($validated['discount'] ?? 0); - $shippingCost = (int) ($validated['shipping_cost'] ?? 0); - $totalAmount = max($subtotal - $discount + $shippingCost, 0); + $totalAmount = max($subtotal - $discount, 0); $channel = OrderChannel::from($validated['channel']); $order = Order::create([ @@ -434,7 +434,6 @@ public function create(array $validated, User $user): Order 'created_by_id' => $user->id, 'subtotal' => $subtotal, 'discount' => $discount, - 'shipping_cost' => $shippingCost, 'marketplace_settings_snapshot' => $this->marketplaceService->buildOrderSnapshot( $channel, $totalAmount, @@ -527,8 +526,7 @@ public function update(Order $order, array $validated): void $lineItems = $this->buildLineItems($validated['items'], $priceType); $subtotal = array_sum(array_column($lineItems, 'subtotal')); $discount = (int) ($validated['discount'] ?? 0); - $shippingCost = (int) ($validated['shipping_cost'] ?? 0); - $totalAmount = max($subtotal - $discount + $shippingCost, 0); + $totalAmount = max($subtotal - $discount, 0); $channel = OrderChannel::from($validated['channel']); $order->customer_id = $validated['customer_id'] ?? null; @@ -541,7 +539,6 @@ public function update(Order $order, array $validated): void $order->shopee_order_id = $validated['shopee_order_id'] ?? null; $order->subtotal = $subtotal; $order->discount = $discount; - $order->shipping_cost = $shippingCost; $order->marketplace_settings_snapshot = $this->marketplaceService->buildOrderSnapshot( $channel, $totalAmount, diff --git a/app/Services/System/AnalysisService.php b/app/Services/System/AnalysisService.php index 952252a..1e6e37b 100644 --- a/app/Services/System/AnalysisService.php +++ b/app/Services/System/AnalysisService.php @@ -152,8 +152,14 @@ public function getTopSuppliers(?Carbon $startDate = null, ?Carbon $endDate = nu public function getTopCustomers(?Carbon $startDate = null, ?Carbon $endDate = null): array { + $user = auth()->user(); + $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; + $isCashier = $user?->hasRole('cashier') ?? false; + return Order::query() ->completed() + ->when($isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when($isCashier, fn ($q) => $q->where('created_by_id', $user->id)) ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) ->join('customers', 'orders.customer_id', '=', 'customers.id') ->selectRaw('customers.name, SUM(orders.total_amount) as total_amount, COUNT(orders.id) as order_count') @@ -171,14 +177,22 @@ public function getTopCustomers(?Carbon $startDate = null, ?Carbon $endDate = nu public function getRevenueSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array { + $user = auth()->user(); + $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; + $isCashier = $user?->hasRole('cashier') ?? false; + $revenueSummary = Order::query() ->completed() + ->when($isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when($isCashier, fn ($q) => $q->where('created_by_id', $user->id)) ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) - ->selectRaw('SUM(total_amount) as total_revenue, SUM(discount) as total_discount, SUM(shipping_cost) as total_shipping, COUNT(*) as total_orders, AVG(total_amount) as avg_order') + ->selectRaw('SUM(total_amount) as total_revenue, SUM(discount) as total_discount, COUNT(*) as total_orders, AVG(total_amount) as avg_order') ->first(); $totalMarketplaceFees = Order::query() ->completed() + ->when($isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when($isCashier, fn ($q) => $q->where('created_by_id', $user->id)) ->whereNotNull('marketplace_settings_snapshot') ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) ->get() @@ -189,7 +203,6 @@ public function getRevenueSummary(?Carbon $startDate = null, ?Carbon $endDate = 'total_discount' => (int) ($revenueSummary->total_discount ?? 0), 'total_marketplace_fees' => $totalMarketplaceFees, 'total_potongan' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees, - 'total_shipping' => (int) ($revenueSummary->total_shipping ?? 0), 'total_orders' => (int) ($revenueSummary->total_orders ?? 0), 'avg_order' => (int) ($revenueSummary->avg_order ?? 0), ]; @@ -197,8 +210,17 @@ public function getRevenueSummary(?Carbon $startDate = null, ?Carbon $endDate = public function getMonthlyRevenue(?Carbon $startDate = null, ?Carbon $endDate = null): array { - $query = Order::query()->completed(); - $feeQuery = Order::query()->completed()->whereNotNull('marketplace_settings_snapshot'); + $user = auth()->user(); + $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; + $isCashier = $user?->hasRole('cashier') ?? false; + + $query = Order::query()->completed() + ->when($isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when($isCashier, fn ($q) => $q->where('created_by_id', $user->id)); + $feeQuery = Order::query()->completed() + ->when($isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when($isCashier, fn ($q) => $q->where('created_by_id', $user->id)) + ->whereNotNull('marketplace_settings_snapshot'); if ($startDate && $endDate) { $query->whereBetween('orders.created_at', [$startDate, $endDate]); @@ -209,8 +231,7 @@ public function getMonthlyRevenue(?Carbon $startDate = null, ?Carbon $endDate = ->selectRaw(" DATE_FORMAT(orders.created_at, '%Y-%m') as month_key, SUM(total_amount) as total_revenue, - SUM(discount) as total_discount, - SUM(shipping_cost) as total_shipping + SUM(discount) as total_discount ") ->groupBy('month_key') ->orderBy('month_key') @@ -245,7 +266,6 @@ public function getMonthlyRevenue(?Carbon $startDate = null, ?Carbon $endDate = 'total' => (int) ($revenue->total_revenue ?? 0), 'net' => (int) ($revenue->total_revenue ?? 0) - $potongan, 'potongan' => $potongan, - 'ongkir' => (int) ($revenue->total_shipping ?? 0), ]; $current->addMonth(); @@ -353,7 +373,13 @@ public function getMonthlyExpense(?Carbon $startDate = null, ?Carbon $endDate = public function getProfitMetrics(?Carbon $startDate = null, ?Carbon $endDate = null): array { - $orderQuery = Order::query()->completed(); + $user = auth()->user(); + $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; + $isCashier = $user?->hasRole('cashier') ?? false; + + $orderQuery = Order::query()->completed() + ->when($isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when($isCashier, fn ($q) => $q->where('created_by_id', $user->id)); if ($startDate && $endDate) { $orderQuery->whereBetween('orders.created_at', [$startDate, $endDate]); } @@ -498,7 +524,13 @@ public function getProductStock(): array public function getBusyHours(?Carbon $startDate = null, ?Carbon $endDate = null): array { + $user = auth()->user(); + $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; + $isCashier = $user?->hasRole('cashier') ?? false; + $hourlyData = Order::query() + ->when($isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when($isCashier, fn ($q) => $q->where('created_by_id', $user->id)) ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) ->selectRaw('HOUR(orders.created_at) as hour, COUNT(*) as order_count') ->groupBy('hour') @@ -519,11 +551,17 @@ public function getBusyHours(?Carbon $startDate = null, ?Carbon $endDate = null) public function getTopProducts(?Carbon $startDate = null, ?Carbon $endDate = null): array { + $user = auth()->user(); + $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; + $isCashier = $user?->hasRole('cashier') ?? false; + return OrderItem::query() ->join('orders', 'order_items.order_id', '=', 'orders.id') ->join('product_variants', 'order_items.product_variant_id', '=', 'product_variants.id') ->join('products', 'product_variants.product_id', '=', 'products.id') ->where('orders.status', OrderStatus::COMPLETED) + ->when($isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id)) + ->when($isCashier, fn ($q) => $q->where('orders.created_by_id', $user->id)) ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) ->selectRaw("CONCAT(products.name, ' - ', product_variants.name) as full_name, SUM(order_items.quantity) as total_qty, SUM(order_items.subtotal) as total_revenue") ->groupBy('order_items.product_variant_id', 'products.name', 'product_variants.name') diff --git a/app/Services/System/DashboardService.php b/app/Services/System/DashboardService.php index 21fa3f3..929ebf9 100644 --- a/app/Services/System/DashboardService.php +++ b/app/Services/System/DashboardService.php @@ -111,14 +111,22 @@ public function getCashOverview(): array public function getRevenueSummary(): array { + $user = auth()->user(); + $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; + $isCashier = $user?->hasRole('cashier') ?? false; + $revenueSummary = Order::query() ->completed() + ->when($isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when($isCashier, fn ($q) => $q->where('created_by_id', $user->id)) ->whereDate('created_at', Carbon::today()) - ->selectRaw('SUM(total_amount) as total_revenue, SUM(discount) as total_discount, SUM(shipping_cost) as total_shipping, COUNT(*) as total_orders, AVG(total_amount) as avg_order') + ->selectRaw('SUM(total_amount) as total_revenue, SUM(discount) as total_discount, COUNT(*) as total_orders, AVG(total_amount) as avg_order') ->first(); $totalMarketplaceFees = Order::query() ->completed() + ->when($isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when($isCashier, fn ($q) => $q->where('created_by_id', $user->id)) ->whereNotNull('marketplace_settings_snapshot') ->whereDate('created_at', Carbon::today()) ->get() @@ -129,7 +137,6 @@ public function getRevenueSummary(): array 'total_discount' => (int) ($revenueSummary->total_discount ?? 0), 'total_marketplace_fees' => $totalMarketplaceFees, 'total_potongan' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees, - 'total_shipping' => (int) ($revenueSummary->total_shipping ?? 0), 'total_orders' => (int) ($revenueSummary->total_orders ?? 0), 'avg_order' => (int) ($revenueSummary->avg_order ?? 0), ]; @@ -171,7 +178,13 @@ public function getExpenseSummary(): array public function getOrderStats(): array { + $user = auth()->user(); + $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; + $isCashier = $user?->hasRole('cashier') ?? false; + $byChannel = Order::query() + ->when($isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when($isCashier, fn ($q) => $q->where('created_by_id', $user->id)) ->whereDate('created_at', Carbon::today()) ->selectRaw('channel, COUNT(*) as count, SUM(total_amount) as total') ->groupBy('channel') @@ -184,6 +197,8 @@ public function getOrderStats(): array ]); $byPaymentType = Order::query() + ->when($isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when($isCashier, fn ($q) => $q->where('created_by_id', $user->id)) ->whereDate('created_at', Carbon::today()) ->selectRaw('payment_type, COUNT(*) as count, SUM(total_amount) as total') ->groupBy('payment_type') @@ -196,6 +211,8 @@ public function getOrderStats(): array ]); $byMarketing = Order::query() + ->when($isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when($isCashier, fn ($q) => $q->where('created_by_id', $user->id)) ->whereNotNull('marketing_id') ->whereDate('orders.created_at', Carbon::today()) ->join('users', 'orders.marketing_id', '=', 'users.id') @@ -212,6 +229,8 @@ public function getOrderStats(): array ]); $byStatus = Order::query() + ->when($isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when($isCashier, fn ($q) => $q->where('created_by_id', $user->id)) ->whereDate('created_at', Carbon::today()) ->selectRaw('status, COUNT(*) as count') ->groupBy('status') diff --git a/app/Support/OwnerVerification/VerificationChangeFormatter.php b/app/Support/OwnerVerification/VerificationChangeFormatter.php index b9a09ea..260b875 100644 --- a/app/Support/OwnerVerification/VerificationChangeFormatter.php +++ b/app/Support/OwnerVerification/VerificationChangeFormatter.php @@ -64,7 +64,6 @@ private static function label(string $field): string 'supplier_id' => 'Supplier', 'subtotal' => 'Subtotal', 'discount' => 'Diskon', - 'shipping_cost' => 'Ongkir', 'total' => 'Total', 'notes' => 'Keterangan', 'items' => 'Item Belanja', diff --git a/database/migrations/2026_06_27_120001_add_paid_amount_to_employee_advances_table.php b/database/migrations/2026_06_27_120001_add_paid_amount_to_employee_advances_table.php new file mode 100644 index 0000000..9de644d --- /dev/null +++ b/database/migrations/2026_06_27_120001_add_paid_amount_to_employee_advances_table.php @@ -0,0 +1,22 @@ +unsignedBigInteger('paid_amount')->default(0)->after('amount'); + }); + } + + public function down(): void + { + Schema::table('employee_advances', function (Blueprint $table) { + $table->dropColumn('paid_amount'); + }); + } +}; diff --git a/database/migrations/2026_06_27_120002_drop_shipping_cost_from_orders_table.php b/database/migrations/2026_06_27_120002_drop_shipping_cost_from_orders_table.php new file mode 100644 index 0000000..af2ade9 --- /dev/null +++ b/database/migrations/2026_06_27_120002_drop_shipping_cost_from_orders_table.php @@ -0,0 +1,22 @@ +dropColumn('shipping_cost'); + }); + } + + public function down(): void + { + Schema::table('orders', function (Blueprint $table) { + $table->unsignedBigInteger('shipping_cost')->default(0)->after('discount'); + }); + } +}; diff --git a/database/seeders/EmployeeSeeder.php b/database/seeders/EmployeeSeeder.php index e1a67f4..30b8a34 100644 --- a/database/seeders/EmployeeSeeder.php +++ b/database/seeders/EmployeeSeeder.php @@ -33,10 +33,15 @@ public function run(): void 'email' => 'direktur@gmail.com', 'full_name' => 'Direktur', ], - Role::MARKETING->value => [ - 'username' => 'marketing', - 'email' => 'marketing@gmail.com', - 'full_name' => 'Marketing', + Role::MARKETING_OFFLINE->value => [ + 'username' => 'marketing-offline', + 'email' => 'marketing.offline@gmail.com', + 'full_name' => 'Marketing Offline', + ], + Role::MARKETING_ONLINE->value => [ + 'username' => 'marketing-online', + 'email' => 'marketing.online@gmail.com', + 'full_name' => 'Marketing Online', ], Role::CASHIER->value => [ 'username' => 'cashier', diff --git a/resources/js/components/AppSidebar.vue b/resources/js/components/AppSidebar.vue index 4946428..4335808 100644 --- a/resources/js/components/AppSidebar.vue +++ b/resources/js/components/AppSidebar.vue @@ -56,7 +56,7 @@ const menuGroups: MenuGroup[] = [ items: [ { title: 'Belanja', href: admin.manage.purchases.index.url(), icon: ShoppingBag, permission: 'purchases.view' }, { title: 'Cutting', href: admin.manage.cuttings.index.url(), icon: Scissors, permission: 'cuttings.view' }, - { title: 'Stok', href: admin.manage.stocks.index.url(), icon: Warehouse, permission: 'stocks.view', badgeKey: 'pendingCuttings' }, + { title: 'Stok Gudang', href: admin.manage.stocks.index.url(), icon: Warehouse, permission: 'stocks.view', badgeKey: 'pendingCuttings' }, { title: 'Verifikasi Owner', href: admin.manage.owner_verifications.index.url(), icon: CheckCircle, permission: 'owner_verifications.view', badgeKey: 'pendingOwnerVerifications' }, { title: 'Pesanan', href: admin.manage.orders.index.url(), icon: ShoppingCart, permission: 'orders.view' }, ], diff --git a/resources/js/constants/employee-advance-status.ts b/resources/js/constants/employee-advance-status.ts index 72b2ee5..70452f9 100644 --- a/resources/js/constants/employee-advance-status.ts +++ b/resources/js/constants/employee-advance-status.ts @@ -2,5 +2,6 @@ export const EmployeeAdvanceStatus = { PENDING: 'pending', APPROVED: 'approved', REJECTED: 'rejected', + PARTIALLY_PAID: 'partially_paid', PAID: 'paid', } as const; diff --git a/resources/js/lib/thermal-printer/encode-order-receipt.ts b/resources/js/lib/thermal-printer/encode-order-receipt.ts index e706485..8fc68d1 100644 --- a/resources/js/lib/thermal-printer/encode-order-receipt.ts +++ b/resources/js/lib/thermal-printer/encode-order-receipt.ts @@ -97,14 +97,9 @@ export function encodeOrderReceipt( const summaryRows: string[][] = [ ['Subtotal', order.subtotal_formatted], ['Diskon', order.discount_formatted], + ['Total', order.total_amount_formatted], ]; - if (order.shipping_cost > 0) { - summaryRows.push(['Ongkir', order.shipping_cost_formatted]); - } - - summaryRows.push(['Total', order.total_amount_formatted]); - encoder.table( [ { width: labelColumnWidth, align: 'left' }, diff --git a/resources/js/pages/admin/Analysis.vue b/resources/js/pages/admin/Analysis.vue index d60b954..3bdeee6 100644 --- a/resources/js/pages/admin/Analysis.vue +++ b/resources/js/pages/admin/Analysis.vue @@ -71,7 +71,6 @@ const props = defineProps<{ total_discount: number; total_marketplace_fees: number; total_potongan: number; - total_shipping: number; total_orders: number; avg_order: number; }; @@ -80,7 +79,6 @@ const props = defineProps<{ total: number; net: number; potongan: number; - ongkir: number; }>; expenseSummary: { total: number; @@ -168,7 +166,7 @@ function onPresetChange(value: any) { } // Revenue Chart -type MonthlyRevenueData = { month: string; total: number; net: number; potongan: number; ongkir: number }; +type MonthlyRevenueData = { month: string; total: number; net: number; potongan: number }; const revenueChartConfig = { total: { @@ -183,19 +181,14 @@ const revenueChartConfig = { label: 'Potongan', color: 'var(--chart-3)', }, - ongkir: { - label: 'Ongkir', - color: 'var(--chart-4)', - }, } satisfies ChartConfig; -const activeRevenueChart = ref<'total' | 'net' | 'potongan' | 'ongkir'>('total'); +const activeRevenueChart = ref<'total' | 'net' | 'potongan'>('total'); const revenueTotals = computed(() => ({ total: props.revenueSummary.total_revenue, net: props.revenueSummary.total_revenue - props.revenueSummary.total_potongan, potongan: props.revenueSummary.total_potongan, - ongkir: props.revenueSummary.total_shipping, })); // Expense Chart @@ -458,7 +451,7 @@ watch([startDate, endDate], () => { Pendapatan
-