diff --git a/app/Http/Controllers/Admin/Manage/TransactionController.php b/app/Http/Controllers/Admin/Manage/TransactionController.php index 4ba8bcd..84c965a 100644 --- a/app/Http/Controllers/Admin/Manage/TransactionController.php +++ b/app/Http/Controllers/Admin/Manage/TransactionController.php @@ -5,6 +5,7 @@ use App\Enums\OrderChannel; use App\Enums\PaymentType; use App\Enums\PriceType; +use App\Enums\Role; use App\Http\Controllers\Controller; use App\Http\Requests\Admin\Manage\TransactionRequest; use App\Http\Requests\PaginatedRequest; @@ -45,13 +46,20 @@ public function index(PaginatedRequest $request): Response public function create(): Response { + $user = auth()->user(); + $isCashier = $user->hasRole(Role::CASHIER); + return Inertia::render('admin/manage/transaction/create', [ 'products' => $this->productVariantService->getForTransaction(), 'customers' => $this->customerService->getAll(), 'employees' => $this->getEmployees(), 'channelOptions' => OrderChannel::toSelect(), 'paymentTypeOptions' => PaymentType::toSelect(), - 'priceTypeOptions' => PriceType::toSelect()->filter(fn ($p) => ! in_array($p['value'], [PriceType::CAPITAL->value, PriceType::REJECT_CAPITAL->value]))->values(), + 'priceTypeOptions' => PriceType::toSelect() + ->filter(fn ($p) => ! in_array($p['value'], [PriceType::CAPITAL->value, PriceType::REJECT_CAPITAL->value])) + ->when($isCashier, fn ($q) => $q->filter(fn ($p) => in_array($p['value'], [PriceType::RETAIL->value, PriceType::REJECT_SELLING->value]))) + ->when(! $isCashier, fn ($q) => $q->filter(fn ($p) => $p['value'] !== PriceType::RETAIL->value)) + ->values(), ]); } @@ -67,6 +75,9 @@ public function store(TransactionRequest $request): RedirectResponse public function edit(Order $transaction): Response { + $user = auth()->user(); + $isCashier = $user->hasRole(Role::CASHIER); + return Inertia::render('admin/manage/transaction/edit', [ 'transaction' => $this->service->getForEdit($transaction), 'products' => $this->productVariantService->getForTransaction(), @@ -74,7 +85,11 @@ public function edit(Order $transaction): Response 'employees' => $this->getEmployees(), 'channelOptions' => OrderChannel::toSelect(), 'paymentTypeOptions' => PaymentType::toSelect(), - 'priceTypeOptions' => PriceType::toSelect()->filter(fn ($p) => ! in_array($p['value'], [PriceType::CAPITAL->value, PriceType::REJECT_CAPITAL->value]))->values(), + 'priceTypeOptions' => PriceType::toSelect() + ->filter(fn ($p) => ! in_array($p['value'], [PriceType::CAPITAL->value, PriceType::REJECT_CAPITAL->value])) + ->when($isCashier, fn ($q) => $q->filter(fn ($p) => in_array($p['value'], [PriceType::RETAIL->value, PriceType::REJECT_SELLING->value]))) + ->when(! $isCashier, fn ($q) => $q->filter(fn ($p) => $p['value'] !== PriceType::RETAIL->value)) + ->values(), ]); } diff --git a/app/Services/Admin/Manage/TransactionService.php b/app/Services/Admin/Manage/TransactionService.php index 117c8a1..2eeae1d 100644 --- a/app/Services/Admin/Manage/TransactionService.php +++ b/app/Services/Admin/Manage/TransactionService.php @@ -253,7 +253,7 @@ public function update(Order $order, array $data): Order $order = DB::transaction(function () use ($order, $data) { $order->load('orderItems'); - $oldStockType = $order->orderItems->first()?->stock_type?->value ?? ProductStockQuality::GOOD->value; + $oldStockType = $order->orderItems->first()?->stock_quality?->value ?? ProductStockQuality::GOOD->value; $order->orderItems->each(function (OrderItem $item) use ($oldStockType) { $this->adjustVariantStock($item->product_variant_id, $item->quantity, 1, $oldStockType); @@ -328,11 +328,13 @@ public function destroy(Order $order): bool $result = DB::transaction(function () use ($order) { $order->load('orderItems'); - $stockType = $order->orderItems->first()?->stock_type?->value ?? ProductStockQuality::GOOD->value; + if (! in_array($order->status, [OrderStatus::CANCELLED, OrderStatus::REFUNDED])) { + $stockType = $order->orderItems->first()?->stock_quality?->value ?? ProductStockQuality::GOOD->value; - $order->orderItems->each(function (OrderItem $item) use ($stockType) { - $this->adjustVariantStock($item->product_variant_id, $item->quantity, 1, $stockType); - }); + $order->orderItems->each(function (OrderItem $item) use ($stockType) { + $this->adjustVariantStock($item->product_variant_id, $item->quantity, 1, $stockType); + }); + } $order->orderItems()->delete(); $order->delete(); @@ -352,8 +354,19 @@ public function destroy(Order $order): bool public function updateStatus(Order $order, string $status): Order { + $oldStatus = $order->status->value; + $order->update(['status' => $status]); + if (in_array($status, [OrderStatus::CANCELLED->value, OrderStatus::REFUNDED->value]) && ! in_array($oldStatus, [OrderStatus::CANCELLED->value, OrderStatus::REFUNDED->value])) { + $order->load('orderItems'); + $stockType = $order->orderItems->first()?->stock_quality?->value ?? ProductStockQuality::GOOD->value; + + $order->orderItems->each(function (OrderItem $item) use ($stockType) { + $this->adjustVariantStock($item->product_variant_id, $item->quantity, 1, $stockType); + }); + } + return $order; } diff --git a/app/Services/Admin/Master/Product/ProductVariantService.php b/app/Services/Admin/Master/Product/ProductVariantService.php index 3d813bc..45b97d9 100644 --- a/app/Services/Admin/Master/Product/ProductVariantService.php +++ b/app/Services/Admin/Master/Product/ProductVariantService.php @@ -73,7 +73,7 @@ public function getForTransaction(): array $products = Product::query() ->select(['id', 'name', 'status']) ->with([ - 'productVariants:id,product_id,name,stock,reject_stock', + 'productVariants:id,product_id,name,stock,reject_stock,retail_stock', 'productVariants.productPrices:id,variant_id,type,price', ]) ->active() diff --git a/app/Services/Concerns/HasStockAdjustment.php b/app/Services/Concerns/HasStockAdjustment.php index 67126ed..fa2ed0a 100644 --- a/app/Services/Concerns/HasStockAdjustment.php +++ b/app/Services/Concerns/HasStockAdjustment.php @@ -13,6 +13,7 @@ trait HasStockAdjustment private const QUALITY_STOCK_MAP = [ ProductStockQuality::GOOD->value => 'stock', ProductStockQuality::REJECT->value => 'reject_stock', + ProductStockQuality::RETAIL->value => 'retail_stock', ]; private function adjustStock(Model $model, string $field, int $quantity, int $sign): void diff --git a/resources/js/pages/admin/manage/transaction/columns.tsx b/resources/js/pages/admin/manage/transaction/columns.tsx index 3324319..fb77e5a 100644 --- a/resources/js/pages/admin/manage/transaction/columns.tsx +++ b/resources/js/pages/admin/manage/transaction/columns.tsx @@ -1,8 +1,8 @@ -export type TransactionStockType = 'good' | 'reject'; +export type TransactionStockType = 'good' | 'reject' | 'retail'; export type TransactionChannel = 'offline' | 'online' | 'whatsapp' | 'shopee' | 'tiktok'; -export type TransactionPriceType = 'distributor' | 'agent' | 'sub_agent' | 'wholesale' | 'retail' | 'tiktok' | 'shopee'; +export type TransactionPriceType = 'distributor' | 'agent' | 'sub_agent' | 'wholesale' | 'retail' | 'tiktok' | 'shopee' | 'reject_selling'; export type TransactionPaymentType = 'cash' | 'transfer' | 'marketplace' | 'qris'; @@ -109,6 +109,7 @@ export type ProductForTransaction = { name: string; stock: number; reject_stock: number; + retail_stock: number; photo_url: string | null; prices: Record; }[]; diff --git a/resources/js/pages/admin/manage/transaction/create.tsx b/resources/js/pages/admin/manage/transaction/create.tsx index 32735ad..14493bc 100644 --- a/resources/js/pages/admin/manage/transaction/create.tsx +++ b/resources/js/pages/admin/manage/transaction/create.tsx @@ -5,11 +5,11 @@ import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2 } from 'lucide-react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { toast } from 'sonner'; import { ConfirmDialog } from '@/components/dialogs'; -import { FileUpload } from '@/components/inputs'; import { ImagePreviewModal } from '@/components/dialogs'; -import { InputError } from '@/components/ui'; +import { FileUpload } from '@/components/inputs'; import { NumberInput } from '@/components/inputs'; import { RupiahInput } from '@/components/inputs'; +import { InputError } from '@/components/ui'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { @@ -39,6 +39,7 @@ import { } from '@/components/ui/sheet'; import { Switch } from '@/components/ui/switch'; import { Textarea } from '@/components/ui/textarea'; +import { useCan } from '@/hooks/use-can'; import { useTransactionDraftSave } from '@/hooks/use-transaction-draft'; import { formatNumber } from '@/lib/format'; import { loadTransactionDraft } from '@/lib/transaction-draft'; @@ -80,6 +81,8 @@ export default function TransactionCreate({ }: Props) { const { auth } = usePage().props as { auth: { user?: { id?: number } } }; const userId = auth.user?.id; + const { hasRole } = useCan(); + const isCashier = hasRole('cashier'); const draft = loadTransactionDraft('create', userId); @@ -87,7 +90,7 @@ export default function TransactionCreate({ draft?.stockType === 'reject' ? 'reject' : 'good', ); const [channel, setChannel] = useState(draft?.channel ?? 'store'); - const [priceType, setPriceType] = useState(draft?.priceType ?? 'retail'); + const [priceType, setPriceType] = useState(draft?.priceType ?? (isCashier ? 'retail' : 'retail')); const [paymentType, setPaymentType] = useState(draft?.paymentType ?? 'cash'); const [customerId, setCustomerId] = useState(draft?.customerId ?? null); const [marketingId, setMarketingId] = useState(draft?.marketingId ?? null); @@ -176,16 +179,20 @@ export default function TransactionCreate({ } else if (channel === 'shopee') { setPriceType('shopee'); setPaymentType('marketplace'); + } else if (isCashier) { + setPriceType('retail'); } - }, [channel]); + }, [channel, isCashier]); useEffect(() => { if (stockType === 'reject') { setPriceType('reject_selling'); + } else if (isCashier) { + setPriceType('retail'); } else if (priceType === 'reject_selling') { setPriceType('retail'); } - }, [stockType]); + }, [stockType, isCashier]); const showPhoto = paymentType === 'transfer' || paymentType === 'qris'; @@ -194,8 +201,12 @@ export default function TransactionCreate({ return priceTypeOptions.filter((o) => o.value === 'reject_selling'); } - return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value)); - }, [stockType, priceTypeOptions]); + if (isCashier) { + return priceTypeOptions.filter((o) => o.value === 'retail'); + } + + return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value) && o.value !== 'retail'); + }, [stockType, priceTypeOptions, isCashier]); const getUnitPrice = useCallback( (variantId: number) => { @@ -209,9 +220,13 @@ export default function TransactionCreate({ return variant.prices?.reject_selling ?? 0; } + if (isCashier) { + return variant.prices?.retail ?? 0; + } + return variant.prices?.[priceType] ?? 0; }, - [variantById, stockType, priceType], + [variantById, stockType, priceType, isCashier], ); const subtotal = Object.entries(quantities).reduce( @@ -236,8 +251,10 @@ export default function TransactionCreate({ (variantId: number, amount: number) => { if (amount > 0 && getUnitPrice(variantId) <= 0) { toast.error('Harga produk ini belum diatur.'); + return; } + setQuantities((prev) => ({ ...prev, [variantId]: Math.max(0, (prev[variantId] ?? 0) + amount), @@ -282,9 +299,9 @@ export default function TransactionCreate({ function getPayload() { return { - stock_type: stockType, + stock_type: stockType === 'good' && isCashier ? 'retail' : stockType, channel, - price_type: stockType === 'reject' ? 'reject_selling' : priceType, + price_type: stockType === 'reject' ? 'reject_selling' : (isCashier ? 'retail' : priceType), payment_type: paymentType, customer_id: customerId, marketing_id: marketingId, @@ -391,9 +408,9 @@ export default function TransactionCreate({ {selectedProduct.product_variants.map( (variant) => { const currentStock = - stockType === 'good' - ? variant.stock - : variant.reject_stock; + stockType === 'reject' + ? variant.reject_stock + : (isCashier ? variant.retail_stock : variant.stock); return (
@@ -629,33 +646,35 @@ export default function TransactionCreate({
)} -
- - - -
+ {!isCashier && ( +
+ + + +
+ )}
diff --git a/resources/js/pages/admin/manage/transaction/edit.tsx b/resources/js/pages/admin/manage/transaction/edit.tsx index e86c1f3..14828e4 100644 --- a/resources/js/pages/admin/manage/transaction/edit.tsx +++ b/resources/js/pages/admin/manage/transaction/edit.tsx @@ -5,11 +5,11 @@ import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2 } from 'lucide-react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { toast } from 'sonner'; import { ConfirmDialog } from '@/components/dialogs'; -import { FileUpload } from '@/components/inputs'; import { ImagePreviewModal } from '@/components/dialogs'; -import { InputError } from '@/components/ui'; +import { FileUpload } from '@/components/inputs'; import { NumberInput } from '@/components/inputs'; import { RupiahInput } from '@/components/inputs'; +import { InputError } from '@/components/ui'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { @@ -39,6 +39,7 @@ import { } from '@/components/ui/sheet'; import { Switch } from '@/components/ui/switch'; import { Textarea } from '@/components/ui/textarea'; +import { useCan } from '@/hooks/use-can'; import { formatNumber } from '@/lib/format'; import { getTemporaryUrl } from '@/lib/upload'; import { formatCurrency } from '@/lib/utils'; @@ -79,6 +80,9 @@ export default function TransactionEdit({ priceTypeOptions, }: Props) { + const { hasRole } = useCan(); + const isCashier = hasRole('cashier'); + const [stockType, setStockType] = useState<'good' | 'reject'>( transaction.stock_type === 'reject' ? 'reject' : 'good', ); @@ -129,16 +133,20 @@ export default function TransactionEdit({ } else if (channel === 'shopee') { setPriceType('shopee'); setPaymentType('marketplace'); + } else if (isCashier) { + setPriceType('retail'); } - }, [channel]); + }, [channel, isCashier]); useEffect(() => { if (stockType === 'reject') { setPriceType('reject_selling'); + } else if (isCashier) { + setPriceType('retail'); } else if (priceType === 'reject_selling') { setPriceType('retail'); } - }, [stockType]); + }, [stockType, isCashier]); const [tiktokOrderId, setTiktokOrderId] = useState(transaction.tiktok_order_id ?? ''); const [shopeeOrderId, setShopeeOrderId] = useState(transaction.shopee_order_id ?? ''); @@ -165,8 +173,12 @@ export default function TransactionEdit({ return priceTypeOptions.filter((o) => o.value === 'reject_selling'); } - return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value)); - }, [stockType, priceTypeOptions]); + if (isCashier) { + return priceTypeOptions.filter((o) => o.value === 'retail'); + } + + return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value) && o.value !== 'retail'); + }, [stockType, priceTypeOptions, isCashier]); const getUnitPrice = useCallback( (variantId: number) => { @@ -180,9 +192,13 @@ export default function TransactionEdit({ return variant.prices?.reject_selling ?? 0; } + if (isCashier) { + return variant.prices?.retail ?? 0; + } + return variant.prices?.[priceType] ?? 0; }, - [variantById, stockType, priceType], + [variantById, stockType, priceType, isCashier], ); const subtotal = Object.entries(quantities).reduce( @@ -207,8 +223,10 @@ export default function TransactionEdit({ (variantId: number, amount: number) => { if (amount > 0 && getUnitPrice(variantId) <= 0) { toast.error('Harga produk ini belum diatur.'); + return; } + setQuantities((prev) => ({ ...prev, [variantId]: Math.max(0, (prev[variantId] ?? 0) + amount), @@ -253,9 +271,9 @@ export default function TransactionEdit({ function getPayload() { return { - stock_type: stockType, + stock_type: stockType === 'good' && isCashier ? 'retail' : stockType, channel, - price_type: stockType === 'reject' ? 'reject_selling' : priceType, + price_type: stockType === 'reject' ? 'reject_selling' : (isCashier ? 'retail' : priceType), payment_type: paymentType, customer_id: customerId, marketing_id: marketingId, @@ -371,9 +389,9 @@ export default function TransactionEdit({ {selectedProduct.product_variants.map( (variant) => { const currentStock = - stockType === 'good' - ? variant.stock - : variant.reject_stock; + stockType === 'reject' + ? variant.reject_stock + : (isCashier ? variant.retail_stock : variant.stock); return (
@@ -609,33 +627,35 @@ export default function TransactionEdit({
)} -
- - - -
+ {!isCashier && ( +
+ + + +
+ )}