feat: enhance transaction management by adding cashier role checks and updating price type logic
This commit is contained in:
parent
85ba5631b3
commit
731c8aa6da
@ -5,6 +5,7 @@
|
|||||||
use App\Enums\OrderChannel;
|
use App\Enums\OrderChannel;
|
||||||
use App\Enums\PaymentType;
|
use App\Enums\PaymentType;
|
||||||
use App\Enums\PriceType;
|
use App\Enums\PriceType;
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\Manage\TransactionRequest;
|
use App\Http\Requests\Admin\Manage\TransactionRequest;
|
||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
@ -45,13 +46,20 @@ public function index(PaginatedRequest $request): Response
|
|||||||
|
|
||||||
public function create(): Response
|
public function create(): Response
|
||||||
{
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$isCashier = $user->hasRole(Role::CASHIER);
|
||||||
|
|
||||||
return Inertia::render('admin/manage/transaction/create', [
|
return Inertia::render('admin/manage/transaction/create', [
|
||||||
'products' => $this->productVariantService->getForTransaction(),
|
'products' => $this->productVariantService->getForTransaction(),
|
||||||
'customers' => $this->customerService->getAll(),
|
'customers' => $this->customerService->getAll(),
|
||||||
'employees' => $this->getEmployees(),
|
'employees' => $this->getEmployees(),
|
||||||
'channelOptions' => OrderChannel::toSelect(),
|
'channelOptions' => OrderChannel::toSelect(),
|
||||||
'paymentTypeOptions' => PaymentType::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
|
public function edit(Order $transaction): Response
|
||||||
{
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$isCashier = $user->hasRole(Role::CASHIER);
|
||||||
|
|
||||||
return Inertia::render('admin/manage/transaction/edit', [
|
return Inertia::render('admin/manage/transaction/edit', [
|
||||||
'transaction' => $this->service->getForEdit($transaction),
|
'transaction' => $this->service->getForEdit($transaction),
|
||||||
'products' => $this->productVariantService->getForTransaction(),
|
'products' => $this->productVariantService->getForTransaction(),
|
||||||
@ -74,7 +85,11 @@ public function edit(Order $transaction): Response
|
|||||||
'employees' => $this->getEmployees(),
|
'employees' => $this->getEmployees(),
|
||||||
'channelOptions' => OrderChannel::toSelect(),
|
'channelOptions' => OrderChannel::toSelect(),
|
||||||
'paymentTypeOptions' => PaymentType::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(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -253,7 +253,7 @@ public function update(Order $order, array $data): Order
|
|||||||
$order = DB::transaction(function () use ($order, $data) {
|
$order = DB::transaction(function () use ($order, $data) {
|
||||||
$order->load('orderItems');
|
$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) {
|
$order->orderItems->each(function (OrderItem $item) use ($oldStockType) {
|
||||||
$this->adjustVariantStock($item->product_variant_id, $item->quantity, 1, $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) {
|
$result = DB::transaction(function () use ($order) {
|
||||||
$order->load('orderItems');
|
$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) {
|
$order->orderItems->each(function (OrderItem $item) use ($stockType) {
|
||||||
$this->adjustVariantStock($item->product_variant_id, $item->quantity, 1, $stockType);
|
$this->adjustVariantStock($item->product_variant_id, $item->quantity, 1, $stockType);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$order->orderItems()->delete();
|
$order->orderItems()->delete();
|
||||||
$order->delete();
|
$order->delete();
|
||||||
@ -352,8 +354,19 @@ public function destroy(Order $order): bool
|
|||||||
|
|
||||||
public function updateStatus(Order $order, string $status): Order
|
public function updateStatus(Order $order, string $status): Order
|
||||||
{
|
{
|
||||||
|
$oldStatus = $order->status->value;
|
||||||
|
|
||||||
$order->update(['status' => $status]);
|
$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;
|
return $order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -73,7 +73,7 @@ public function getForTransaction(): array
|
|||||||
$products = Product::query()
|
$products = Product::query()
|
||||||
->select(['id', 'name', 'status'])
|
->select(['id', 'name', 'status'])
|
||||||
->with([
|
->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',
|
'productVariants.productPrices:id,variant_id,type,price',
|
||||||
])
|
])
|
||||||
->active()
|
->active()
|
||||||
|
|||||||
@ -13,6 +13,7 @@ trait HasStockAdjustment
|
|||||||
private const QUALITY_STOCK_MAP = [
|
private const QUALITY_STOCK_MAP = [
|
||||||
ProductStockQuality::GOOD->value => 'stock',
|
ProductStockQuality::GOOD->value => 'stock',
|
||||||
ProductStockQuality::REJECT->value => 'reject_stock',
|
ProductStockQuality::REJECT->value => 'reject_stock',
|
||||||
|
ProductStockQuality::RETAIL->value => 'retail_stock',
|
||||||
];
|
];
|
||||||
|
|
||||||
private function adjustStock(Model $model, string $field, int $quantity, int $sign): void
|
private function adjustStock(Model $model, string $field, int $quantity, int $sign): void
|
||||||
|
|||||||
@ -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 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';
|
export type TransactionPaymentType = 'cash' | 'transfer' | 'marketplace' | 'qris';
|
||||||
|
|
||||||
@ -109,6 +109,7 @@ export type ProductForTransaction = {
|
|||||||
name: string;
|
name: string;
|
||||||
stock: number;
|
stock: number;
|
||||||
reject_stock: number;
|
reject_stock: number;
|
||||||
|
retail_stock: number;
|
||||||
photo_url: string | null;
|
photo_url: string | null;
|
||||||
prices: Record<string, number>;
|
prices: Record<string, number>;
|
||||||
}[];
|
}[];
|
||||||
|
|||||||
@ -5,11 +5,11 @@ import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { ConfirmDialog } from '@/components/dialogs';
|
import { ConfirmDialog } from '@/components/dialogs';
|
||||||
import { FileUpload } from '@/components/inputs';
|
|
||||||
import { ImagePreviewModal } from '@/components/dialogs';
|
import { ImagePreviewModal } from '@/components/dialogs';
|
||||||
import { InputError } from '@/components/ui';
|
import { FileUpload } from '@/components/inputs';
|
||||||
import { NumberInput } from '@/components/inputs';
|
import { NumberInput } from '@/components/inputs';
|
||||||
import { RupiahInput } from '@/components/inputs';
|
import { RupiahInput } from '@/components/inputs';
|
||||||
|
import { InputError } from '@/components/ui';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import {
|
import {
|
||||||
@ -39,6 +39,7 @@ import {
|
|||||||
} from '@/components/ui/sheet';
|
} from '@/components/ui/sheet';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { useCan } from '@/hooks/use-can';
|
||||||
import { useTransactionDraftSave } from '@/hooks/use-transaction-draft';
|
import { useTransactionDraftSave } from '@/hooks/use-transaction-draft';
|
||||||
import { formatNumber } from '@/lib/format';
|
import { formatNumber } from '@/lib/format';
|
||||||
import { loadTransactionDraft } from '@/lib/transaction-draft';
|
import { loadTransactionDraft } from '@/lib/transaction-draft';
|
||||||
@ -80,6 +81,8 @@ export default function TransactionCreate({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const { auth } = usePage().props as { auth: { user?: { id?: number } } };
|
const { auth } = usePage().props as { auth: { user?: { id?: number } } };
|
||||||
const userId = auth.user?.id;
|
const userId = auth.user?.id;
|
||||||
|
const { hasRole } = useCan();
|
||||||
|
const isCashier = hasRole('cashier');
|
||||||
|
|
||||||
const draft = loadTransactionDraft('create', userId);
|
const draft = loadTransactionDraft('create', userId);
|
||||||
|
|
||||||
@ -87,7 +90,7 @@ export default function TransactionCreate({
|
|||||||
draft?.stockType === 'reject' ? 'reject' : 'good',
|
draft?.stockType === 'reject' ? 'reject' : 'good',
|
||||||
);
|
);
|
||||||
const [channel, setChannel] = useState(draft?.channel ?? 'store');
|
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 [paymentType, setPaymentType] = useState(draft?.paymentType ?? 'cash');
|
||||||
const [customerId, setCustomerId] = useState<number | null>(draft?.customerId ?? null);
|
const [customerId, setCustomerId] = useState<number | null>(draft?.customerId ?? null);
|
||||||
const [marketingId, setMarketingId] = useState<number | null>(draft?.marketingId ?? null);
|
const [marketingId, setMarketingId] = useState<number | null>(draft?.marketingId ?? null);
|
||||||
@ -176,16 +179,20 @@ export default function TransactionCreate({
|
|||||||
} else if (channel === 'shopee') {
|
} else if (channel === 'shopee') {
|
||||||
setPriceType('shopee');
|
setPriceType('shopee');
|
||||||
setPaymentType('marketplace');
|
setPaymentType('marketplace');
|
||||||
|
} else if (isCashier) {
|
||||||
|
setPriceType('retail');
|
||||||
}
|
}
|
||||||
}, [channel]);
|
}, [channel, isCashier]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (stockType === 'reject') {
|
if (stockType === 'reject') {
|
||||||
setPriceType('reject_selling');
|
setPriceType('reject_selling');
|
||||||
|
} else if (isCashier) {
|
||||||
|
setPriceType('retail');
|
||||||
} else if (priceType === 'reject_selling') {
|
} else if (priceType === 'reject_selling') {
|
||||||
setPriceType('retail');
|
setPriceType('retail');
|
||||||
}
|
}
|
||||||
}, [stockType]);
|
}, [stockType, isCashier]);
|
||||||
|
|
||||||
const showPhoto = paymentType === 'transfer' || paymentType === 'qris';
|
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) => o.value === 'reject_selling');
|
||||||
}
|
}
|
||||||
|
|
||||||
return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value));
|
if (isCashier) {
|
||||||
}, [stockType, priceTypeOptions]);
|
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(
|
const getUnitPrice = useCallback(
|
||||||
(variantId: number) => {
|
(variantId: number) => {
|
||||||
@ -209,9 +220,13 @@ export default function TransactionCreate({
|
|||||||
return variant.prices?.reject_selling ?? 0;
|
return variant.prices?.reject_selling ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isCashier) {
|
||||||
|
return variant.prices?.retail ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
return variant.prices?.[priceType] ?? 0;
|
return variant.prices?.[priceType] ?? 0;
|
||||||
},
|
},
|
||||||
[variantById, stockType, priceType],
|
[variantById, stockType, priceType, isCashier],
|
||||||
);
|
);
|
||||||
|
|
||||||
const subtotal = Object.entries(quantities).reduce(
|
const subtotal = Object.entries(quantities).reduce(
|
||||||
@ -236,8 +251,10 @@ export default function TransactionCreate({
|
|||||||
(variantId: number, amount: number) => {
|
(variantId: number, amount: number) => {
|
||||||
if (amount > 0 && getUnitPrice(variantId) <= 0) {
|
if (amount > 0 && getUnitPrice(variantId) <= 0) {
|
||||||
toast.error('Harga produk ini belum diatur.');
|
toast.error('Harga produk ini belum diatur.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setQuantities((prev) => ({
|
setQuantities((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[variantId]: Math.max(0, (prev[variantId] ?? 0) + amount),
|
[variantId]: Math.max(0, (prev[variantId] ?? 0) + amount),
|
||||||
@ -282,9 +299,9 @@ export default function TransactionCreate({
|
|||||||
|
|
||||||
function getPayload() {
|
function getPayload() {
|
||||||
return {
|
return {
|
||||||
stock_type: stockType,
|
stock_type: stockType === 'good' && isCashier ? 'retail' : stockType,
|
||||||
channel,
|
channel,
|
||||||
price_type: stockType === 'reject' ? 'reject_selling' : priceType,
|
price_type: stockType === 'reject' ? 'reject_selling' : (isCashier ? 'retail' : priceType),
|
||||||
payment_type: paymentType,
|
payment_type: paymentType,
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
marketing_id: marketingId,
|
marketing_id: marketingId,
|
||||||
@ -391,9 +408,9 @@ export default function TransactionCreate({
|
|||||||
{selectedProduct.product_variants.map(
|
{selectedProduct.product_variants.map(
|
||||||
(variant) => {
|
(variant) => {
|
||||||
const currentStock =
|
const currentStock =
|
||||||
stockType === 'good'
|
stockType === 'reject'
|
||||||
? variant.stock
|
? variant.reject_stock
|
||||||
: variant.reject_stock;
|
: (isCashier ? variant.retail_stock : variant.stock);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -444,7 +461,7 @@ export default function TransactionCreate({
|
|||||||
formatCurrency(
|
formatCurrency(
|
||||||
stockType === 'reject'
|
stockType === 'reject'
|
||||||
? (variant.prices?.reject_selling ?? 0)
|
? (variant.prices?.reject_selling ?? 0)
|
||||||
: (variant.prices?.[priceType] ?? 0),
|
: (isCashier ? (variant.prices?.retail ?? 0) : (variant.prices?.[priceType] ?? 0)),
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
@ -629,33 +646,35 @@ export default function TransactionCreate({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid gap-2">
|
{!isCashier && (
|
||||||
<Label>Tipe Harga <span className="text-destructive">*</span></Label>
|
<div className="grid gap-2">
|
||||||
<Select
|
<Label>Tipe Harga <span className="text-destructive">*</span></Label>
|
||||||
value={priceType}
|
<Select
|
||||||
onValueChange={setPriceType}
|
value={priceType}
|
||||||
disabled={channel === 'tiktok' || channel === 'shopee'}
|
onValueChange={setPriceType}
|
||||||
>
|
disabled={channel === 'tiktok' || channel === 'shopee'}
|
||||||
<SelectTrigger className="w-full">
|
>
|
||||||
<SelectValue placeholder="Pilih tipe harga" />
|
<SelectTrigger className="w-full">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Pilih tipe harga" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
{availablePriceTypes.map(
|
<SelectContent>
|
||||||
(opt) => (
|
{availablePriceTypes.map(
|
||||||
<SelectItem
|
(opt) => (
|
||||||
key={opt.value}
|
<SelectItem
|
||||||
value={opt.value}
|
key={opt.value}
|
||||||
>
|
value={opt.value}
|
||||||
{opt.label}
|
>
|
||||||
</SelectItem>
|
{opt.label}
|
||||||
),
|
</SelectItem>
|
||||||
)}
|
),
|
||||||
</SelectContent>
|
)}
|
||||||
</Select>
|
</SelectContent>
|
||||||
<InputError
|
</Select>
|
||||||
message={errors.price_type}
|
<InputError
|
||||||
/>
|
message={errors.price_type}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>Tipe Pembayaran <span className="text-destructive">*</span></Label>
|
<Label>Tipe Pembayaran <span className="text-destructive">*</span></Label>
|
||||||
|
|||||||
@ -5,11 +5,11 @@ import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { ConfirmDialog } from '@/components/dialogs';
|
import { ConfirmDialog } from '@/components/dialogs';
|
||||||
import { FileUpload } from '@/components/inputs';
|
|
||||||
import { ImagePreviewModal } from '@/components/dialogs';
|
import { ImagePreviewModal } from '@/components/dialogs';
|
||||||
import { InputError } from '@/components/ui';
|
import { FileUpload } from '@/components/inputs';
|
||||||
import { NumberInput } from '@/components/inputs';
|
import { NumberInput } from '@/components/inputs';
|
||||||
import { RupiahInput } from '@/components/inputs';
|
import { RupiahInput } from '@/components/inputs';
|
||||||
|
import { InputError } from '@/components/ui';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import {
|
import {
|
||||||
@ -39,6 +39,7 @@ import {
|
|||||||
} from '@/components/ui/sheet';
|
} from '@/components/ui/sheet';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { useCan } from '@/hooks/use-can';
|
||||||
import { formatNumber } from '@/lib/format';
|
import { formatNumber } from '@/lib/format';
|
||||||
import { getTemporaryUrl } from '@/lib/upload';
|
import { getTemporaryUrl } from '@/lib/upload';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
@ -79,6 +80,9 @@ export default function TransactionEdit({
|
|||||||
priceTypeOptions,
|
priceTypeOptions,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
|
||||||
|
const { hasRole } = useCan();
|
||||||
|
const isCashier = hasRole('cashier');
|
||||||
|
|
||||||
const [stockType, setStockType] = useState<'good' | 'reject'>(
|
const [stockType, setStockType] = useState<'good' | 'reject'>(
|
||||||
transaction.stock_type === 'reject' ? 'reject' : 'good',
|
transaction.stock_type === 'reject' ? 'reject' : 'good',
|
||||||
);
|
);
|
||||||
@ -129,16 +133,20 @@ export default function TransactionEdit({
|
|||||||
} else if (channel === 'shopee') {
|
} else if (channel === 'shopee') {
|
||||||
setPriceType('shopee');
|
setPriceType('shopee');
|
||||||
setPaymentType('marketplace');
|
setPaymentType('marketplace');
|
||||||
|
} else if (isCashier) {
|
||||||
|
setPriceType('retail');
|
||||||
}
|
}
|
||||||
}, [channel]);
|
}, [channel, isCashier]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (stockType === 'reject') {
|
if (stockType === 'reject') {
|
||||||
setPriceType('reject_selling');
|
setPriceType('reject_selling');
|
||||||
|
} else if (isCashier) {
|
||||||
|
setPriceType('retail');
|
||||||
} else if (priceType === 'reject_selling') {
|
} else if (priceType === 'reject_selling') {
|
||||||
setPriceType('retail');
|
setPriceType('retail');
|
||||||
}
|
}
|
||||||
}, [stockType]);
|
}, [stockType, isCashier]);
|
||||||
|
|
||||||
const [tiktokOrderId, setTiktokOrderId] = useState(transaction.tiktok_order_id ?? '');
|
const [tiktokOrderId, setTiktokOrderId] = useState(transaction.tiktok_order_id ?? '');
|
||||||
const [shopeeOrderId, setShopeeOrderId] = useState(transaction.shopee_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) => o.value === 'reject_selling');
|
||||||
}
|
}
|
||||||
|
|
||||||
return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value));
|
if (isCashier) {
|
||||||
}, [stockType, priceTypeOptions]);
|
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(
|
const getUnitPrice = useCallback(
|
||||||
(variantId: number) => {
|
(variantId: number) => {
|
||||||
@ -180,9 +192,13 @@ export default function TransactionEdit({
|
|||||||
return variant.prices?.reject_selling ?? 0;
|
return variant.prices?.reject_selling ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isCashier) {
|
||||||
|
return variant.prices?.retail ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
return variant.prices?.[priceType] ?? 0;
|
return variant.prices?.[priceType] ?? 0;
|
||||||
},
|
},
|
||||||
[variantById, stockType, priceType],
|
[variantById, stockType, priceType, isCashier],
|
||||||
);
|
);
|
||||||
|
|
||||||
const subtotal = Object.entries(quantities).reduce(
|
const subtotal = Object.entries(quantities).reduce(
|
||||||
@ -207,8 +223,10 @@ export default function TransactionEdit({
|
|||||||
(variantId: number, amount: number) => {
|
(variantId: number, amount: number) => {
|
||||||
if (amount > 0 && getUnitPrice(variantId) <= 0) {
|
if (amount > 0 && getUnitPrice(variantId) <= 0) {
|
||||||
toast.error('Harga produk ini belum diatur.');
|
toast.error('Harga produk ini belum diatur.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setQuantities((prev) => ({
|
setQuantities((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[variantId]: Math.max(0, (prev[variantId] ?? 0) + amount),
|
[variantId]: Math.max(0, (prev[variantId] ?? 0) + amount),
|
||||||
@ -253,9 +271,9 @@ export default function TransactionEdit({
|
|||||||
|
|
||||||
function getPayload() {
|
function getPayload() {
|
||||||
return {
|
return {
|
||||||
stock_type: stockType,
|
stock_type: stockType === 'good' && isCashier ? 'retail' : stockType,
|
||||||
channel,
|
channel,
|
||||||
price_type: stockType === 'reject' ? 'reject_selling' : priceType,
|
price_type: stockType === 'reject' ? 'reject_selling' : (isCashier ? 'retail' : priceType),
|
||||||
payment_type: paymentType,
|
payment_type: paymentType,
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
marketing_id: marketingId,
|
marketing_id: marketingId,
|
||||||
@ -371,9 +389,9 @@ export default function TransactionEdit({
|
|||||||
{selectedProduct.product_variants.map(
|
{selectedProduct.product_variants.map(
|
||||||
(variant) => {
|
(variant) => {
|
||||||
const currentStock =
|
const currentStock =
|
||||||
stockType === 'good'
|
stockType === 'reject'
|
||||||
? variant.stock
|
? variant.reject_stock
|
||||||
: variant.reject_stock;
|
: (isCashier ? variant.retail_stock : variant.stock);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -424,7 +442,7 @@ export default function TransactionEdit({
|
|||||||
formatCurrency(
|
formatCurrency(
|
||||||
stockType === 'reject'
|
stockType === 'reject'
|
||||||
? (variant.prices?.reject_selling ?? 0)
|
? (variant.prices?.reject_selling ?? 0)
|
||||||
: (variant.prices?.[priceType] ?? 0),
|
: (isCashier ? (variant.prices?.retail ?? 0) : (variant.prices?.[priceType] ?? 0)),
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
@ -609,33 +627,35 @@ export default function TransactionEdit({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid gap-2">
|
{!isCashier && (
|
||||||
<Label>Tipe Harga <span className="text-destructive">*</span></Label>
|
<div className="grid gap-2">
|
||||||
<Select
|
<Label>Tipe Harga <span className="text-destructive">*</span></Label>
|
||||||
value={priceType}
|
<Select
|
||||||
onValueChange={setPriceType}
|
value={priceType}
|
||||||
disabled={channel === 'tiktok' || channel === 'shopee'}
|
onValueChange={setPriceType}
|
||||||
>
|
disabled={channel === 'tiktok' || channel === 'shopee'}
|
||||||
<SelectTrigger className="w-full">
|
>
|
||||||
<SelectValue placeholder="Pilih tipe harga" />
|
<SelectTrigger className="w-full">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Pilih tipe harga" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
{availablePriceTypes.map(
|
<SelectContent>
|
||||||
(opt) => (
|
{availablePriceTypes.map(
|
||||||
<SelectItem
|
(opt) => (
|
||||||
key={opt.value}
|
<SelectItem
|
||||||
value={opt.value}
|
key={opt.value}
|
||||||
>
|
value={opt.value}
|
||||||
{opt.label}
|
>
|
||||||
</SelectItem>
|
{opt.label}
|
||||||
),
|
</SelectItem>
|
||||||
)}
|
),
|
||||||
</SelectContent>
|
)}
|
||||||
</Select>
|
</SelectContent>
|
||||||
<InputError
|
</Select>
|
||||||
message={errors.price_type}
|
<InputError
|
||||||
/>
|
message={errors.price_type}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>Tipe Pembayaran <span className="text-destructive">*</span></Label>
|
<Label>Tipe Pembayaran <span className="text-destructive">*</span></Label>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user