feat: add partially paid status to employee advances and update related functionalities
This commit is contained in:
parent
eb5eeaf73b
commit
7c0b59b998
@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('employee_advances', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('paid_amount')->default(0)->after('amount');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('employee_advances', function (Blueprint $table) {
|
||||
$table->dropColumn('paid_amount');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('orders', function (Blueprint $table) {
|
||||
$table->dropColumn('shipping_cost');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('orders', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('shipping_cost')->default(0)->after('discount');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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',
|
||||
|
||||
@ -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' },
|
||||
],
|
||||
|
||||
@ -2,5 +2,6 @@ export const EmployeeAdvanceStatus = {
|
||||
PENDING: 'pending',
|
||||
APPROVED: 'approved',
|
||||
REJECTED: 'rejected',
|
||||
PARTIALLY_PAID: 'partially_paid',
|
||||
PAID: 'paid',
|
||||
} as const;
|
||||
|
||||
@ -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' },
|
||||
|
||||
@ -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], () => {
|
||||
<CardTitle>Pendapatan</CardTitle>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<button v-for="chart in ['total', 'net', 'potongan', 'ongkir'] as const" :key="chart"
|
||||
<button v-for="chart in ['total', 'net', 'potongan'] as const" :key="chart"
|
||||
:data-active="activeRevenueChart === chart"
|
||||
class="data-[active=true]:bg-muted/50 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l sm:border-t-0 sm:border-l sm:px-8 sm:py-6"
|
||||
@click="activeRevenueChart = chart">
|
||||
|
||||
@ -47,7 +47,6 @@ interface DashboardProps {
|
||||
total_discount: number;
|
||||
total_marketplace_fees: number;
|
||||
total_potongan: number;
|
||||
total_shipping: number;
|
||||
total_orders: number;
|
||||
avg_order: number;
|
||||
};
|
||||
|
||||
@ -10,6 +10,8 @@ function statusVariant(status: string): 'default' | 'secondary' | 'destructive'
|
||||
switch (status) {
|
||||
case EmployeeAdvanceStatus.APPROVED:
|
||||
return 'default';
|
||||
case EmployeeAdvanceStatus.PARTIALLY_PAID:
|
||||
return 'outline';
|
||||
case EmployeeAdvanceStatus.PAID:
|
||||
return 'secondary';
|
||||
case EmployeeAdvanceStatus.REJECTED:
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { router } from '@inertiajs/vue3';
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { RowApproveAction, RowDeleteAction, RowEditAction, RowPayAction, RowRejectAction } from '@/components/button';
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import { parseRupiah } from '@/lib/rupiah';
|
||||
import { destroy, approve, pay } from '@/routes/admin/finance/employee_advances';
|
||||
import type { EmployeeAdvanceListItem } from '@/types/employee-advance';
|
||||
|
||||
@ -43,6 +46,15 @@ const approveConfirmOpen = ref(false);
|
||||
const payConfirmOpen = ref(false);
|
||||
const approveProcessing = ref(false);
|
||||
const payProcessing = ref(false);
|
||||
const payAmount = ref('');
|
||||
const payError = ref('');
|
||||
|
||||
watch(payConfirmOpen, (open) => {
|
||||
if (open) {
|
||||
payAmount.value = String(props.employeeAdvance.remaining_amount);
|
||||
payError.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
function approveEmployeeAdvance() {
|
||||
approveProcessing.value = true;
|
||||
@ -64,9 +76,22 @@ function approveEmployeeAdvance() {
|
||||
}
|
||||
|
||||
function payEmployeeAdvance() {
|
||||
const amount = Number(parseRupiah(payAmount.value));
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
payError.value = 'Jumlah pembayaran harus lebih dari 0.';
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount > props.employeeAdvance.remaining_amount) {
|
||||
payError.value = 'Jumlah pembayaran melebihi sisa kasbon.';
|
||||
return;
|
||||
}
|
||||
|
||||
payError.value = '';
|
||||
payProcessing.value = true;
|
||||
|
||||
router.post(pay.url(props.employeeAdvance.id), {}, {
|
||||
router.post(pay.url(props.employeeAdvance.id), { amount }, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
payConfirmOpen.value = false;
|
||||
@ -93,7 +118,7 @@ function payEmployeeAdvance() {
|
||||
<RowRejectAction v-if="employeeAdvance.can_verify && can('employee_advances.verify')" tooltip="Tolak"
|
||||
@click="emit('reject', employeeAdvance)" />
|
||||
|
||||
<RowPayAction v-if="employeeAdvance.can_pay && can('employee_advances.pay')" tooltip="Lunasi Kasbon"
|
||||
<RowPayAction v-if="employeeAdvance.can_pay && can('employee_advances.pay')" tooltip="Bayar Kasbon"
|
||||
@click="payConfirmOpen = true" />
|
||||
|
||||
<template v-if="canDelete">
|
||||
@ -107,7 +132,23 @@ function payEmployeeAdvance() {
|
||||
:description="`Kasbon ${employeeAdvance.amount_formatted} untuk ${employeeAdvance.employee_name} akan dicairkan. Saldo kas akan berkurang.`"
|
||||
confirm-label="Setujui" cancel-label="Batal" :loading="approveProcessing" @confirm="approveEmployeeAdvance" />
|
||||
|
||||
<ConfirmDialog v-if="can('employee_advances.pay')" v-model:open="payConfirmOpen" title="Lunasi kasbon?"
|
||||
:description="`Pelunasan kasbon ${employeeAdvance.amount_formatted} dari ${employeeAdvance.employee_name}. Saldo kas akan bertambah.`"
|
||||
confirm-label="Lunasi" cancel-label="Batal" :loading="payProcessing" @confirm="payEmployeeAdvance" />
|
||||
<ConfirmDialog v-if="can('employee_advances.pay')" v-model:open="payConfirmOpen"
|
||||
:title="employeeAdvance.remaining_amount < employeeAdvance.amount ? 'Bayar sisa kasbon?' : 'Bayar kasbon?'"
|
||||
:description="`Pembayaran kasbon ${employeeAdvance.employee_name}. Sisa: ${employeeAdvance.remaining_amount_formatted}.`"
|
||||
confirm-label="Bayar" cancel-label="Batal" :loading="payProcessing" @confirm="payEmployeeAdvance">
|
||||
<template #content>
|
||||
<div class="space-y-3 py-2">
|
||||
<div class="text-sm text-muted-foreground">
|
||||
<p>Total kasbon: <strong>{{ employeeAdvance.amount_formatted }}</strong></p>
|
||||
<p>Sudah dibayar: <strong>{{ employeeAdvance.paid_amount_formatted }}</strong></p>
|
||||
<p>Sisa: <strong class="text-primary">{{ employeeAdvance.remaining_amount_formatted }}</strong></p>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel for="pay-amount" required>Jumlah Pembayaran</FieldLabel>
|
||||
<RupiahInput id="pay-amount" v-model="payAmount" placeholder="0" />
|
||||
<FieldError v-if="payError" :errors="{ amount: [payError] }" />
|
||||
</Field>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</template>
|
||||
|
||||
@ -36,7 +36,6 @@ const initialData = computed(() => ({
|
||||
tiktok_order_id: props.order.tiktok_order_id ?? '',
|
||||
shopee_order_id: props.order.shopee_order_id ?? '',
|
||||
discount: String(props.order.discount),
|
||||
shipping_cost: String(props.order.shipping_cost),
|
||||
notes: props.order.notes ?? '',
|
||||
items: props.order.items.map((item) => ({
|
||||
product_variant_id: item.product_variant_id,
|
||||
|
||||
@ -10,7 +10,6 @@ import {
|
||||
Receipt,
|
||||
Send,
|
||||
Trash2,
|
||||
Truck,
|
||||
User,
|
||||
X,
|
||||
} from '@lucide/vue';
|
||||
@ -143,10 +142,6 @@ const summaryRows = computed(() => {
|
||||
rows.push({ label: 'Diskon', value: `-${props.order.discount_formatted}` });
|
||||
}
|
||||
|
||||
if (props.order.shipping_cost > 0) {
|
||||
rows.push({ label: 'Ongkos Kirim', value: props.order.shipping_cost_formatted });
|
||||
}
|
||||
|
||||
return rows;
|
||||
});
|
||||
|
||||
@ -523,25 +518,6 @@ const feeEntries = computed(() => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Shipping Info -->
|
||||
<Card v-if="order.shipping_cost > 0">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Truck class="size-5" />
|
||||
Pengiriman
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Ongkos Kirim</span>
|
||||
<span class="font-medium tabular-nums">
|
||||
{{ order.shipping_cost_formatted }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
|
||||
@ -74,7 +74,6 @@ const props = defineProps<{
|
||||
tiktok_order_id: string;
|
||||
shopee_order_id: string;
|
||||
discount: string;
|
||||
shipping_cost: string;
|
||||
notes: string;
|
||||
items: OrderCartItem[];
|
||||
};
|
||||
@ -95,7 +94,7 @@ const { can } = useCan();
|
||||
const page = usePage();
|
||||
const authUser = computed(() => (page.props.auth as Auth).user);
|
||||
const isMarketingUser = computed(() =>
|
||||
authUser.value?.roles?.includes('marketing') ?? false,
|
||||
(authUser.value?.roles?.includes('marketing-offline') || authUser.value?.roles?.includes('marketing-online')) ?? false,
|
||||
);
|
||||
|
||||
const search = ref('');
|
||||
@ -126,7 +125,6 @@ const form = useForm({
|
||||
tiktok_order_id: '',
|
||||
shopee_order_id: '',
|
||||
discount: '',
|
||||
shipping_cost: '',
|
||||
notes: '',
|
||||
});
|
||||
|
||||
@ -147,7 +145,6 @@ function populateForm() {
|
||||
form.tiktok_order_id = props.initialData.tiktok_order_id;
|
||||
form.shopee_order_id = props.initialData.shopee_order_id;
|
||||
form.discount = props.initialData.discount;
|
||||
form.shipping_cost = String(props.initialData.shipping_cost);
|
||||
form.notes = props.initialData.notes;
|
||||
cart.value = props.initialData.items.map((item) => ({
|
||||
...item,
|
||||
@ -253,8 +250,7 @@ const subtotal = computed(() =>
|
||||
);
|
||||
|
||||
const discountAmount = computed(() => Number(parseRupiah(form.discount)) || 0);
|
||||
const shippingAmount = computed(() => Number(parseRupiah(form.shipping_cost)) || 0);
|
||||
const totalAmount = computed(() => Math.max(subtotal.value - discountAmount.value + shippingAmount.value, 0));
|
||||
const totalAmount = computed(() => Math.max(subtotal.value - discountAmount.value, 0));
|
||||
|
||||
function getVariantPrice(variant: ProductVariantItem): ProductPriceItem | undefined {
|
||||
return variant.prices.find((price) => price.type === form.price_type);
|
||||
@ -460,7 +456,6 @@ function buildFormData(): FormData {
|
||||
}
|
||||
|
||||
formData.append('discount', parseRupiah(form.discount));
|
||||
formData.append('shipping_cost', parseRupiah(form.shipping_cost));
|
||||
formData.append('notes', form.notes);
|
||||
|
||||
if (isCreateMode.value && printAfterSave.value) {
|
||||
@ -822,11 +817,6 @@ function submit() {
|
||||
<RupiahInput id="discount" v-model="form.discount" placeholder="0" />
|
||||
<FieldError :errors="formErrors(form, 'discount')" />
|
||||
</Field>
|
||||
<Field v-if="isStoreChannel">
|
||||
<FieldLabel for="shipping_cost">Ongkir</FieldLabel>
|
||||
<RupiahInput id="shipping_cost" v-model="form.shipping_cost" placeholder="0" />
|
||||
<FieldError :errors="formErrors(form, 'shipping_cost')" />
|
||||
</Field>
|
||||
<div class="flex justify-between text-base font-semibold">
|
||||
<span>Total</span>
|
||||
<span class="text-primary">Rp {{ formatRupiah(totalAmount) }}</span>
|
||||
|
||||
@ -126,8 +126,6 @@ function statusVariant(status: string): 'default' | 'secondary' | 'destructive'
|
||||
}}</strong></span>
|
||||
<span>Diskon <strong class="text-primary">{{ order.discount_formatted
|
||||
}}</strong></span>
|
||||
<span v-if="order.shipping_cost > 0">Ongkir <strong class="text-primary">{{
|
||||
order.shipping_cost_formatted }}</strong></span>
|
||||
<span>Total <strong class="text-primary">{{ order.total_amount_formatted
|
||||
}}</strong></span>
|
||||
<span v-if="order.marketplace_settings_snapshot?.total_fee_amount" class="text-destructive">
|
||||
|
||||
@ -12,7 +12,7 @@ const props = defineProps<{
|
||||
const { can } = useCan();
|
||||
|
||||
const isSystemRole = computed(() => {
|
||||
return ['developer', 'owner', 'admin-toko', 'admin-bahan-baku', 'direktur', 'marketing', 'cashier', 'non-operator'].includes(props.role.name);
|
||||
return ['developer', 'owner', 'admin-toko', 'admin-bahan-baku', 'direktur', 'marketing-offline', 'marketing-online', 'cashier', 'non-operator'].includes(props.role.name);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -3,6 +3,10 @@ export type EmployeeAdvanceListItem = {
|
||||
employee_id: number;
|
||||
amount: number;
|
||||
amount_formatted: string;
|
||||
paid_amount: number;
|
||||
paid_amount_formatted: string;
|
||||
remaining_amount: number;
|
||||
remaining_amount_formatted: string;
|
||||
description: string;
|
||||
due_date_input: string;
|
||||
due_date_formatted: string;
|
||||
|
||||
@ -52,8 +52,6 @@ export type OrderListItem = {
|
||||
shopee_order_id: string | null;
|
||||
subtotal_formatted: string;
|
||||
discount_formatted: string;
|
||||
shipping_cost: number;
|
||||
shipping_cost_formatted: string;
|
||||
total_amount: number;
|
||||
total_amount_formatted: string;
|
||||
notes: string | null;
|
||||
@ -116,7 +114,6 @@ export type OrderEditItem = {
|
||||
tiktok_order_id: string | null;
|
||||
shopee_order_id: string | null;
|
||||
discount: number;
|
||||
shipping_cost: number;
|
||||
notes: string | null;
|
||||
items: Array<{
|
||||
product_variant_id: number;
|
||||
@ -151,8 +148,6 @@ export type OrderDetail = {
|
||||
subtotal_formatted: string;
|
||||
discount: number;
|
||||
discount_formatted: string;
|
||||
shipping_cost: number;
|
||||
shipping_cost_formatted: string;
|
||||
total_amount: number;
|
||||
total_amount_formatted: string;
|
||||
notes: string | null;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user