feat: split 'reject' price type into 'reject_capital' and 'reject_selling', update related components and database migration
This commit is contained in:
parent
0947cf3737
commit
0998c81803
@ -300,7 +300,7 @@ ## Enums
|
|||||||
| `PayrollPeriodStatus` | open, closed | payroll_periods.status |
|
| `PayrollPeriodStatus` | open, closed | payroll_periods.status |
|
||||||
| `PayrollStatus` | unpaid, paid, cancelled | payrolls.status |
|
| `PayrollStatus` | unpaid, paid, cancelled | payrolls.status |
|
||||||
| `Permission` | — | Permission action names |
|
| `Permission` | — | Permission action names |
|
||||||
| `PriceType` | retail, wholesale, capital | product_prices.type, orders.price_type |
|
| `PriceType` | retail, wholesale, capital, reject_capital, reject_selling | product_prices.type, orders.price_type |
|
||||||
| `ProductStatus` | active, draft, inactive, pending, rejected | products.status |
|
| `ProductStatus` | active, draft, inactive, pending, rejected | products.status |
|
||||||
| `ProductStockQuality` | good, reject | order_items.stock_quality, restocks.stock_type, stok_opname_items.stock_quality |
|
| `ProductStockQuality` | good, reject | order_items.stock_quality, restocks.stock_type, stok_opname_items.stock_quality |
|
||||||
| `RawMaterialUnit` | kg, meter, yard | raw_materials.unit |
|
| `RawMaterialUnit` | kg, meter, yard | raw_materials.unit |
|
||||||
|
|||||||
@ -108,7 +108,7 @@ public function fix(): array
|
|||||||
private function resolvePriceType(string $orderPriceType, string $stockQuality): string
|
private function resolvePriceType(string $orderPriceType, string $stockQuality): string
|
||||||
{
|
{
|
||||||
if ($stockQuality === ProductStockQuality::REJECT->value) {
|
if ($stockQuality === ProductStockQuality::REJECT->value) {
|
||||||
return PriceType::REJECT->value;
|
return PriceType::REJECT_SELLING->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
$map = [
|
$map = [
|
||||||
|
|||||||
@ -16,7 +16,8 @@ enum PriceType: string
|
|||||||
case TIKTOK = 'tiktok';
|
case TIKTOK = 'tiktok';
|
||||||
case SHOPEE = 'shopee';
|
case SHOPEE = 'shopee';
|
||||||
case CAPITAL = 'capital';
|
case CAPITAL = 'capital';
|
||||||
case REJECT = 'reject';
|
case REJECT_CAPITAL = 'reject_capital';
|
||||||
|
case REJECT_SELLING = 'reject_selling';
|
||||||
|
|
||||||
public function label(): string
|
public function label(): string
|
||||||
{
|
{
|
||||||
@ -29,7 +30,8 @@ public function label(): string
|
|||||||
self::TIKTOK => 'TikTok',
|
self::TIKTOK => 'TikTok',
|
||||||
self::SHOPEE => 'Shopee',
|
self::SHOPEE => 'Shopee',
|
||||||
self::CAPITAL => 'Modal',
|
self::CAPITAL => 'Modal',
|
||||||
self::REJECT => 'Reject',
|
self::REJECT_CAPITAL => 'Reject Modal',
|
||||||
|
self::REJECT_SELLING => 'Reject Jual',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ public function rules(): array
|
|||||||
return [
|
return [
|
||||||
'stock_type' => ['sometimes', 'required', Rule::in(ProductStockQuality::values())],
|
'stock_type' => ['sometimes', 'required', Rule::in(ProductStockQuality::values())],
|
||||||
'channel' => ['sometimes', 'required', Rule::in(OrderChannel::values())],
|
'channel' => ['sometimes', 'required', Rule::in(OrderChannel::values())],
|
||||||
'price_type' => ['sometimes', 'required', Rule::in(array_diff(PriceType::values(), [PriceType::CAPITAL->value]))],
|
'price_type' => ['sometimes', 'required', Rule::in(array_diff(PriceType::values(), [PriceType::CAPITAL->value, PriceType::REJECT_CAPITAL->value]))],
|
||||||
'payment_type' => ['sometimes', 'required', Rule::in(PaymentType::values())],
|
'payment_type' => ['sometimes', 'required', Rule::in(PaymentType::values())],
|
||||||
'customer_id' => ['nullable', 'integer', Rule::exists('customers', 'id')],
|
'customer_id' => ['nullable', 'integer', Rule::exists('customers', 'id')],
|
||||||
'marketing_id' => ['nullable', 'integer', Rule::exists('users', 'id')],
|
'marketing_id' => ['nullable', 'integer', Rule::exists('users', 'id')],
|
||||||
|
|||||||
@ -188,7 +188,7 @@ public function destroy(Restock $restock): bool
|
|||||||
private function buildItemRows(array $items, string $stockType, $now, int &$total): array
|
private function buildItemRows(array $items, string $stockType, $now, int &$total): array
|
||||||
{
|
{
|
||||||
$priceType = $stockType === ProductStockQuality::REJECT->value
|
$priceType = $stockType === ProductStockQuality::REJECT->value
|
||||||
? PriceType::REJECT
|
? PriceType::REJECT_CAPITAL
|
||||||
: PriceType::CAPITAL;
|
: PriceType::CAPITAL;
|
||||||
|
|
||||||
$variantIds = collect($items)->pluck('product_variant_id')->unique()->all();
|
$variantIds = collect($items)->pluck('product_variant_id')->unique()->all();
|
||||||
|
|||||||
@ -33,6 +33,7 @@ class TransactionService
|
|||||||
PriceType::RETAIL->value => PriceType::RETAIL,
|
PriceType::RETAIL->value => PriceType::RETAIL,
|
||||||
PriceType::TIKTOK->value => PriceType::TIKTOK,
|
PriceType::TIKTOK->value => PriceType::TIKTOK,
|
||||||
PriceType::SHOPEE->value => PriceType::SHOPEE,
|
PriceType::SHOPEE->value => PriceType::SHOPEE,
|
||||||
|
PriceType::REJECT_SELLING->value => PriceType::REJECT_SELLING,
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@ -355,7 +356,7 @@ public function updateStatus(Order $order, string $status): Order
|
|||||||
private function buildItemRows(array $items, string $stockType, string $priceType, $now, int &$subtotal, int &$totalCost): array
|
private function buildItemRows(array $items, string $stockType, string $priceType, $now, int &$subtotal, int &$totalCost): array
|
||||||
{
|
{
|
||||||
$resolvedPriceType = $stockType === ProductStockQuality::REJECT->value
|
$resolvedPriceType = $stockType === ProductStockQuality::REJECT->value
|
||||||
? PriceType::REJECT
|
? PriceType::REJECT_SELLING
|
||||||
: (self::SELLING_PRICE_MAP[$priceType] ?? PriceType::RETAIL);
|
: (self::SELLING_PRICE_MAP[$priceType] ?? PriceType::RETAIL);
|
||||||
|
|
||||||
$variantIds = collect($items)->pluck('product_variant_id')->unique()->all();
|
$variantIds = collect($items)->pluck('product_variant_id')->unique()->all();
|
||||||
@ -375,9 +376,13 @@ private function buildItemRows(array $items, string $stockType, string $priceTyp
|
|||||||
return [$variant->id => $price?->price ?? 0];
|
return [$variant->id => $price?->price ?? 0];
|
||||||
});
|
});
|
||||||
|
|
||||||
$capitalPrices = $variants->mapWithKeys(function (ProductVariant $variant) {
|
$capitalPriceType = $stockType === ProductStockQuality::REJECT->value
|
||||||
|
? PriceType::REJECT_CAPITAL
|
||||||
|
: PriceType::CAPITAL;
|
||||||
|
|
||||||
|
$capitalPrices = $variants->mapWithKeys(function (ProductVariant $variant) use ($capitalPriceType) {
|
||||||
$price = $variant->productPrices
|
$price = $variant->productPrices
|
||||||
->first(fn ($p) => $p->type === PriceType::CAPITAL);
|
->first(fn ($p) => $p->type === $capitalPriceType);
|
||||||
|
|
||||||
return [$variant->id => $price?->price ?? 0];
|
return [$variant->id => $price?->price ?? 0];
|
||||||
});
|
});
|
||||||
|
|||||||
@ -41,11 +41,11 @@ public function getForRestock(): array
|
|||||||
|
|
||||||
if ($allVariantIds !== []) {
|
if ($allVariantIds !== []) {
|
||||||
$mediaByVariant = Media::query()
|
$mediaByVariant = Media::query()
|
||||||
->whereIn('mediable_id', $allVariantIds)
|
->whereIn('model_id', $allVariantIds)
|
||||||
->where('mediable_type', ProductVariant::class)
|
->where('model_type', ProductVariant::class)
|
||||||
->where('collection_name', 'images')
|
->where('collection_name', 'images')
|
||||||
->get()
|
->get()
|
||||||
->groupBy('mediable_id');
|
->groupBy('model_id');
|
||||||
} else {
|
} else {
|
||||||
$mediaByVariant = collect();
|
$mediaByVariant = collect();
|
||||||
}
|
}
|
||||||
@ -62,7 +62,7 @@ public function getForRestock(): array
|
|||||||
$variant->capital_price = $capitalPrice?->price ?? 0;
|
$variant->capital_price = $capitalPrice?->price ?? 0;
|
||||||
|
|
||||||
$rejectPrice = $variant->productPrices
|
$rejectPrice = $variant->productPrices
|
||||||
->first(fn ($price) => $price->type === PriceType::REJECT);
|
->first(fn ($price) => $price->type === PriceType::REJECT_CAPITAL);
|
||||||
$variant->reject_price = $rejectPrice?->price ?? 0;
|
$variant->reject_price = $rejectPrice?->price ?? 0;
|
||||||
});
|
});
|
||||||
})->toArray();
|
})->toArray();
|
||||||
@ -84,11 +84,11 @@ public function getForTransaction(): array
|
|||||||
|
|
||||||
if ($allVariantIds !== []) {
|
if ($allVariantIds !== []) {
|
||||||
$mediaByVariant = Media::query()
|
$mediaByVariant = Media::query()
|
||||||
->whereIn('mediable_id', $allVariantIds)
|
->whereIn('model_id', $allVariantIds)
|
||||||
->where('mediable_type', ProductVariant::class)
|
->where('model_type', ProductVariant::class)
|
||||||
->where('collection_name', 'images')
|
->where('collection_name', 'images')
|
||||||
->get()
|
->get()
|
||||||
->groupBy('mediable_id');
|
->groupBy('model_id');
|
||||||
} else {
|
} else {
|
||||||
$mediaByVariant = collect();
|
$mediaByVariant = collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// 1. product_prices: rename 'reject' → 'reject_selling', then add 'reject_capital' rows
|
||||||
|
DB::table('product_prices')
|
||||||
|
->where('type', 'reject')
|
||||||
|
->update(['type' => 'reject_selling']);
|
||||||
|
|
||||||
|
// Insert reject_capital rows (price = 0) for each variant that now has reject_selling
|
||||||
|
$rejectSellingPrices = DB::table('product_prices')
|
||||||
|
->where('type', 'reject_selling')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($rejectSellingPrices as $row) {
|
||||||
|
DB::table('product_prices')->insert([
|
||||||
|
'variant_id' => $row->variant_id,
|
||||||
|
'type' => 'reject_capital',
|
||||||
|
'price' => 0,
|
||||||
|
'created_at' => $row->created_at,
|
||||||
|
'updated_at' => $row->updated_at,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. orders: rename 'reject' → 'reject_selling'
|
||||||
|
DB::table('orders')
|
||||||
|
->where('price_type', 'reject')
|
||||||
|
->update(['price_type' => 'reject_selling']);
|
||||||
|
|
||||||
|
// 3. Modify enum columns to reflect new values (MySQL only)
|
||||||
|
if (DB::getDriverName() === 'mysql') {
|
||||||
|
DB::statement("ALTER TABLE product_prices MODIFY COLUMN type ENUM('distributor','agent','sub_agent','wholesale','retail','tiktok','shopee','capital','reject_capital','reject_selling') NOT NULL");
|
||||||
|
DB::statement("ALTER TABLE orders MODIFY COLUMN price_type ENUM('distributor','agent','sub_agent','wholesale','retail','tiktok','shopee','capital','reject_capital','reject_selling') NOT NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Reverse: rename back and clean up
|
||||||
|
DB::table('product_prices')
|
||||||
|
->where('type', 'reject_capital')
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
DB::table('product_prices')
|
||||||
|
->where('type', 'reject_selling')
|
||||||
|
->update(['type' => 'reject']);
|
||||||
|
|
||||||
|
DB::table('orders')
|
||||||
|
->where('price_type', 'reject_selling')
|
||||||
|
->update(['price_type' => 'reject']);
|
||||||
|
|
||||||
|
if (DB::getDriverName() === 'mysql') {
|
||||||
|
DB::statement("ALTER TABLE product_prices MODIFY COLUMN type ENUM('distributor','agent','sub_agent','wholesale','retail','tiktok','shopee','capital','reject') NOT NULL");
|
||||||
|
DB::statement("ALTER TABLE orders MODIFY COLUMN price_type ENUM('distributor','agent','sub_agent','wholesale','retail','tiktok','shopee','capital','reject') NOT NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -10,21 +10,21 @@ export function NotificationPermissionPrompt() {
|
|||||||
usePushNotification();
|
usePushNotification();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isSupported || hasRequested.current) {
|
if (!isSupported || hasRequested.current || !vapidPublicKey) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Notification.permission !== 'default') {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasRequested.current = true;
|
hasRequested.current = true;
|
||||||
|
|
||||||
void requestPermission().then(async (result) => {
|
if (Notification.permission === 'default') {
|
||||||
if (result === 'granted' && vapidPublicKey) {
|
void requestPermission().then(async (result) => {
|
||||||
await subscribe(vapidPublicKey);
|
if (result === 'granted') {
|
||||||
}
|
await subscribe(vapidPublicKey);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
} else if (Notification.permission === 'granted') {
|
||||||
|
void subscribe(vapidPublicKey);
|
||||||
|
}
|
||||||
}, [isSupported, requestPermission, subscribe, vapidPublicKey]);
|
}, [isSupported, requestPermission, subscribe, vapidPublicKey]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -68,7 +68,7 @@ type Props = {
|
|||||||
priceTypeOptions: TransactionCreateData['priceTypeOptions'];
|
priceTypeOptions: TransactionCreateData['priceTypeOptions'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const SELLING_PRICE_TYPES = ['distributor', 'agent', 'sub_agent', 'wholesale', 'retail', 'tiktok', 'shopee'];
|
const SELLING_PRICE_TYPES = ['distributor', 'agent', 'sub_agent', 'wholesale', 'retail', 'tiktok', 'shopee', 'reject_selling'];
|
||||||
|
|
||||||
export default function TransactionCreate({
|
export default function TransactionCreate({
|
||||||
products,
|
products,
|
||||||
@ -181,8 +181,8 @@ export default function TransactionCreate({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (stockType === 'reject') {
|
if (stockType === 'reject') {
|
||||||
setPriceType('reject');
|
setPriceType('reject_selling');
|
||||||
} else if (priceType === 'reject') {
|
} else if (priceType === 'reject_selling') {
|
||||||
setPriceType('retail');
|
setPriceType('retail');
|
||||||
}
|
}
|
||||||
}, [stockType]);
|
}, [stockType]);
|
||||||
@ -191,7 +191,7 @@ export default function TransactionCreate({
|
|||||||
|
|
||||||
const availablePriceTypes = useMemo(() => {
|
const availablePriceTypes = useMemo(() => {
|
||||||
if (stockType === 'reject') {
|
if (stockType === 'reject') {
|
||||||
return priceTypeOptions.filter((o) => o.value === 'reject');
|
return priceTypeOptions.filter((o) => o.value === 'reject_selling');
|
||||||
}
|
}
|
||||||
|
|
||||||
return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value));
|
return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value));
|
||||||
@ -206,7 +206,7 @@ export default function TransactionCreate({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stockType === 'reject') {
|
if (stockType === 'reject') {
|
||||||
return variant.prices?.reject ?? 0;
|
return variant.prices?.reject_selling ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return variant.prices?.[priceType] ?? 0;
|
return variant.prices?.[priceType] ?? 0;
|
||||||
@ -284,7 +284,7 @@ export default function TransactionCreate({
|
|||||||
return {
|
return {
|
||||||
stock_type: stockType,
|
stock_type: stockType,
|
||||||
channel,
|
channel,
|
||||||
price_type: stockType === 'reject' ? 'reject' : priceType,
|
price_type: stockType === 'reject' ? 'reject_selling' : priceType,
|
||||||
payment_type: paymentType,
|
payment_type: paymentType,
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
marketing_id: marketingId,
|
marketing_id: marketingId,
|
||||||
@ -443,7 +443,7 @@ export default function TransactionCreate({
|
|||||||
) : (
|
) : (
|
||||||
formatCurrency(
|
formatCurrency(
|
||||||
stockType === 'reject'
|
stockType === 'reject'
|
||||||
? (variant.prices?.reject ?? 0)
|
? (variant.prices?.reject_selling ?? 0)
|
||||||
: (variant.prices?.[priceType] ?? 0),
|
: (variant.prices?.[priceType] ?? 0),
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -67,7 +67,7 @@ type Props = {
|
|||||||
priceTypeOptions: TransactionCreateData['priceTypeOptions'];
|
priceTypeOptions: TransactionCreateData['priceTypeOptions'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const SELLING_PRICE_TYPES = ['distributor', 'agent', 'sub_agent', 'wholesale', 'retail', 'tiktok', 'shopee'];
|
const SELLING_PRICE_TYPES = ['distributor', 'agent', 'sub_agent', 'wholesale', 'retail', 'tiktok', 'shopee', 'reject_selling'];
|
||||||
|
|
||||||
export default function TransactionEdit({
|
export default function TransactionEdit({
|
||||||
transaction,
|
transaction,
|
||||||
@ -134,8 +134,8 @@ export default function TransactionEdit({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (stockType === 'reject') {
|
if (stockType === 'reject') {
|
||||||
setPriceType('reject');
|
setPriceType('reject_selling');
|
||||||
} else if (priceType === 'reject') {
|
} else if (priceType === 'reject_selling') {
|
||||||
setPriceType('retail');
|
setPriceType('retail');
|
||||||
}
|
}
|
||||||
}, [stockType]);
|
}, [stockType]);
|
||||||
@ -162,7 +162,7 @@ export default function TransactionEdit({
|
|||||||
|
|
||||||
const availablePriceTypes = useMemo(() => {
|
const availablePriceTypes = useMemo(() => {
|
||||||
if (stockType === 'reject') {
|
if (stockType === 'reject') {
|
||||||
return priceTypeOptions.filter((o) => o.value === 'reject');
|
return priceTypeOptions.filter((o) => o.value === 'reject_selling');
|
||||||
}
|
}
|
||||||
|
|
||||||
return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value));
|
return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value));
|
||||||
@ -177,7 +177,7 @@ export default function TransactionEdit({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stockType === 'reject') {
|
if (stockType === 'reject') {
|
||||||
return variant.prices?.reject ?? 0;
|
return variant.prices?.reject_selling ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return variant.prices?.[priceType] ?? 0;
|
return variant.prices?.[priceType] ?? 0;
|
||||||
@ -255,7 +255,7 @@ export default function TransactionEdit({
|
|||||||
return {
|
return {
|
||||||
stock_type: stockType,
|
stock_type: stockType,
|
||||||
channel,
|
channel,
|
||||||
price_type: stockType === 'reject' ? 'reject' : priceType,
|
price_type: stockType === 'reject' ? 'reject_selling' : priceType,
|
||||||
payment_type: paymentType,
|
payment_type: paymentType,
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
marketing_id: marketingId,
|
marketing_id: marketingId,
|
||||||
@ -423,7 +423,7 @@ export default function TransactionEdit({
|
|||||||
) : (
|
) : (
|
||||||
formatCurrency(
|
formatCurrency(
|
||||||
stockType === 'reject'
|
stockType === 'reject'
|
||||||
? (variant.prices?.reject ?? 0)
|
? (variant.prices?.reject_selling ?? 0)
|
||||||
: (variant.prices?.[priceType] ?? 0),
|
: (variant.prices?.[priceType] ?? 0),
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -43,7 +43,8 @@ const PRICE_TYPES = [
|
|||||||
{ key: 'tiktok', label: 'TikTok' },
|
{ key: 'tiktok', label: 'TikTok' },
|
||||||
{ key: 'shopee', label: 'Shopee' },
|
{ key: 'shopee', label: 'Shopee' },
|
||||||
{ key: 'capital', label: 'Modal' },
|
{ key: 'capital', label: 'Modal' },
|
||||||
{ key: 'reject', label: 'Reject' },
|
{ key: 'reject_capital', label: 'Reject Modal' },
|
||||||
|
{ key: 'reject_selling', label: 'Reject Jual' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function createEmptyPrices(): Array<{ type: string; price: number }> {
|
function createEmptyPrices(): Array<{ type: string; price: number }> {
|
||||||
|
|||||||
@ -61,7 +61,8 @@ const PRICE_TYPES = [
|
|||||||
{ key: 'tiktok', label: 'TikTok' },
|
{ key: 'tiktok', label: 'TikTok' },
|
||||||
{ key: 'shopee', label: 'Shopee' },
|
{ key: 'shopee', label: 'Shopee' },
|
||||||
{ key: 'capital', label: 'Modal' },
|
{ key: 'capital', label: 'Modal' },
|
||||||
{ key: 'reject', label: 'Reject' },
|
{ key: 'reject_capital', label: 'Reject Modal' },
|
||||||
|
{ key: 'reject_selling', label: 'Reject Jual' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function createEmptyPrices(): Array<{ type: string; price: number }> {
|
function createEmptyPrices(): Array<{ type: string; price: number }> {
|
||||||
@ -104,6 +105,13 @@ export default function ProductEdit({ product, categories }: Props) {
|
|||||||
product.description ?? '',
|
product.description ?? '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function normalizePrices(serverPrices: Array<{ type: string; price: number }>): Array<{ type: string; price: number }> {
|
||||||
|
return PRICE_TYPES.map((pt) => {
|
||||||
|
const existing = serverPrices.find((p) => p.type === pt.key);
|
||||||
|
return { type: pt.key, price: existing?.price ?? 0 };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const serverVariants: VariantState[] = product.product_variants.map(
|
const serverVariants: VariantState[] = product.product_variants.map(
|
||||||
(v) => ({
|
(v) => ({
|
||||||
id: v.id,
|
id: v.id,
|
||||||
@ -116,7 +124,7 @@ export default function ProductEdit({ product, categories }: Props) {
|
|||||||
url: v.photo_urls[i] ?? null,
|
url: v.photo_urls[i] ?? null,
|
||||||
})),
|
})),
|
||||||
uploading: false,
|
uploading: false,
|
||||||
prices: v.prices.length > 0 ? v.prices : createEmptyPrices(),
|
prices: normalizePrices(v.prices),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,8 @@ const PRICE_TYPES = [
|
|||||||
{ key: 'tiktok', label: 'TikTok' },
|
{ key: 'tiktok', label: 'TikTok' },
|
||||||
{ key: 'shopee', label: 'Shopee' },
|
{ key: 'shopee', label: 'Shopee' },
|
||||||
{ key: 'capital', label: 'Modal' },
|
{ key: 'capital', label: 'Modal' },
|
||||||
{ key: 'reject', label: 'Reject' },
|
{ key: 'reject_capital', label: 'Reject Modal' },
|
||||||
|
{ key: 'reject_selling', label: 'Reject Jual' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function ProductVariantEdit({ variant }: Props) {
|
export default function ProductVariantEdit({ variant }: Props) {
|
||||||
@ -53,14 +54,15 @@ export default function ProductVariantEdit({ variant }: Props) {
|
|||||||
const [prices, setPrices] = useState<
|
const [prices, setPrices] = useState<
|
||||||
Array<{ type: string; price: number }>
|
Array<{ type: string; price: number }>
|
||||||
>(
|
>(
|
||||||
variant.prices.length > 0
|
PRICE_TYPES.map((pt) => {
|
||||||
? variant.prices
|
const existing = variant.prices.find((p) => p.type === pt.key);
|
||||||
: PRICE_TYPES.map((pt) => ({ type: pt.key, price: 0 })),
|
return { type: pt.key, price: existing?.price ?? 0 };
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
function updatePrice(priceIndex: number, value: number) {
|
function updatePrice(priceType: string, value: number) {
|
||||||
setPrices((prev) =>
|
setPrices((prev) =>
|
||||||
prev.map((p, i) => (i === priceIndex ? { ...p, price: value } : p)),
|
prev.map((p) => (p.type === priceType ? { ...p, price: value } : p)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +202,9 @@ export default function ProductVariantEdit({ variant }: Props) {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-3">
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-3">
|
||||||
{PRICE_TYPES.map(
|
{PRICE_TYPES.map(
|
||||||
(priceType, priceIndex) => (
|
(priceType) => {
|
||||||
|
const priceEntry = prices.find((p) => p.type === priceType.key);
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={priceType.key}
|
key={priceType.key}
|
||||||
className="grid gap-2"
|
className="grid gap-2"
|
||||||
@ -214,15 +218,13 @@ export default function ProductVariantEdit({ variant }: Props) {
|
|||||||
</Label>
|
</Label>
|
||||||
<RupiahInput
|
<RupiahInput
|
||||||
value={
|
value={
|
||||||
prices[
|
priceEntry?.price ?? 0
|
||||||
priceIndex
|
|
||||||
]?.price ?? 0
|
|
||||||
}
|
}
|
||||||
onValueChange={(
|
onValueChange={(
|
||||||
val,
|
val,
|
||||||
) =>
|
) =>
|
||||||
updatePrice(
|
updatePrice(
|
||||||
priceIndex,
|
priceType.key,
|
||||||
val,
|
val,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -230,12 +232,13 @@ export default function ProductVariantEdit({ variant }: Props) {
|
|||||||
<InputError
|
<InputError
|
||||||
message={
|
message={
|
||||||
errors[
|
errors[
|
||||||
`prices.${priceIndex}.price`
|
`prices.${priceType.key}.price`
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -45,7 +45,8 @@ function makeValidProductPayload(array $overrides = []): array
|
|||||||
['type' => 'tiktok', 'price' => 16000],
|
['type' => 'tiktok', 'price' => 16000],
|
||||||
['type' => 'shopee', 'price' => 16500],
|
['type' => 'shopee', 'price' => 16500],
|
||||||
['type' => 'capital', 'price' => 8000],
|
['type' => 'capital', 'price' => 8000],
|
||||||
['type' => 'reject', 'price' => 5000],
|
['type' => 'reject_capital', 'price' => 5000],
|
||||||
|
['type' => 'reject_selling', 'price' => 6000],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -71,7 +72,8 @@ function makeValidSharedPricePayload(array $overrides = []): array
|
|||||||
['type' => 'tiktok', 'price' => 16000],
|
['type' => 'tiktok', 'price' => 16000],
|
||||||
['type' => 'shopee', 'price' => 16500],
|
['type' => 'shopee', 'price' => 16500],
|
||||||
['type' => 'capital', 'price' => 8000],
|
['type' => 'capital', 'price' => 8000],
|
||||||
['type' => 'reject', 'price' => 5000],
|
['type' => 'reject_capital', 'price' => 5000],
|
||||||
|
['type' => 'reject_selling', 'price' => 6000],
|
||||||
],
|
],
|
||||||
'variants' => [
|
'variants' => [
|
||||||
[
|
[
|
||||||
@ -97,7 +99,8 @@ function allPriceTypes(): array
|
|||||||
['type' => 'tiktok', 'price' => 16000],
|
['type' => 'tiktok', 'price' => 16000],
|
||||||
['type' => 'shopee', 'price' => 16500],
|
['type' => 'shopee', 'price' => 16500],
|
||||||
['type' => 'capital', 'price' => 8000],
|
['type' => 'capital', 'price' => 8000],
|
||||||
['type' => 'reject', 'price' => 5000],
|
['type' => 'reject_capital', 'price' => 5000],
|
||||||
|
['type' => 'reject_selling', 'price' => 6000],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1946,7 +1949,8 @@ function allPriceTypes(): array
|
|||||||
['type' => 'tiktok', 'price' => 16000],
|
['type' => 'tiktok', 'price' => 16000],
|
||||||
['type' => 'shopee', 'price' => 16500],
|
['type' => 'shopee', 'price' => 16500],
|
||||||
['type' => 'capital', 'price' => 8000],
|
['type' => 'capital', 'price' => 8000],
|
||||||
['type' => 'reject', 'price' => 5000],
|
['type' => 'reject_capital', 'price' => 5000],
|
||||||
|
['type' => 'reject_selling', 'price' => 6000],
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->post(route('admin.master.products.store'), makeValidProductPayload([
|
$response = $this->post(route('admin.master.products.store'), makeValidProductPayload([
|
||||||
@ -1965,9 +1969,10 @@ function allPriceTypes(): array
|
|||||||
$variant = ProductVariant::where('name', 'Semua Harga')->first();
|
$variant = ProductVariant::where('name', 'Semua Harga')->first();
|
||||||
$dbPrices = ProductPrice::where('variant_id', $variant->id)->get();
|
$dbPrices = ProductPrice::where('variant_id', $variant->id)->get();
|
||||||
|
|
||||||
expect($dbPrices)->toHaveCount(9);
|
expect($dbPrices)->toHaveCount(10);
|
||||||
expect($dbPrices->firstWhere('type', 'distributor')->price)->toBe(10000);
|
expect($dbPrices->firstWhere('type', 'distributor')->price)->toBe(10000);
|
||||||
expect($dbPrices->firstWhere('type', 'reject')->price)->toBe(5000);
|
expect($dbPrices->firstWhere('type', 'reject_capital')->price)->toBe(5000);
|
||||||
|
expect($dbPrices->firstWhere('type', 'reject_selling')->price)->toBe(6000);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shared price applies to all variants', function () {
|
test('shared price applies to all variants', function () {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user