Compare commits
No commits in common. "1f395657545d793479028af2c7b7af978895d219" and "6546e1e3ebe732aa415d86b2ee1cd7a733bf95d9" have entirely different histories.
1f39565754
...
6546e1e3eb
@ -16,7 +16,7 @@
|
|||||||
class AdminSettingsController extends Controller
|
class AdminSettingsController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private AdminSettingsService $service
|
private readonly AdminSettingsService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(): Response
|
public function index(): Response
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
class CashAccountController extends Controller
|
class CashAccountController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CashAccountService $service
|
private readonly CashAccountService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
class EmployeeAdvanceController extends Controller
|
class EmployeeAdvanceController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private EmployeeAdvanceService $service
|
private readonly EmployeeAdvanceService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
class ExpenseController extends Controller
|
class ExpenseController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ExpenseService $service
|
private readonly ExpenseService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
class PayrollAdjustmentController extends Controller
|
class PayrollAdjustmentController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private PayrollAdjustmentService $service
|
private readonly PayrollAdjustmentService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function store(PayrollAdjustmentRequest $request, Payroll $payroll): RedirectResponse
|
public function store(PayrollAdjustmentRequest $request, Payroll $payroll): RedirectResponse
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
class PayrollController extends Controller
|
class PayrollController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private PayrollPeriodService $service
|
private readonly PayrollPeriodService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function pay(Payroll $payroll): RedirectResponse
|
public function pay(Payroll $payroll): RedirectResponse
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
class PayrollPeriodController extends Controller
|
class PayrollPeriodController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private PayrollPeriodService $service
|
private readonly PayrollPeriodService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
class AttendanceController extends Controller
|
class AttendanceController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private AttendanceService $service
|
private readonly AttendanceService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
class EmployeeController extends Controller
|
class EmployeeController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private EmployeeService $service
|
private readonly EmployeeService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
class LeaveRequestController extends Controller
|
class LeaveRequestController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private LeaveRequestService $service
|
private readonly LeaveRequestService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
class CuttingController extends Controller
|
class CuttingController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CuttingService $service,
|
private readonly CuttingService $service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -7,7 +7,6 @@
|
|||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\Purchase;
|
use App\Models\Purchase;
|
||||||
use App\Services\Admin\Manage\PurchaseService;
|
use App\Services\Admin\Manage\PurchaseService;
|
||||||
use App\Services\Admin\Master\SupplierService;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
@ -16,7 +15,6 @@ class PurchaseController extends Controller
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PurchaseService $service,
|
private readonly PurchaseService $service,
|
||||||
private readonly SupplierService $supplierService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
@ -24,10 +22,7 @@ public function index(PaginatedRequest $request): Response
|
|||||||
return Inertia::render('admin/manage/purchase/index', [
|
return Inertia::render('admin/manage/purchase/index', [
|
||||||
'purchases' => $this->service->paginated(
|
'purchases' => $this->service->paginated(
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
filters: $request->only(['supplier_id']),
|
|
||||||
),
|
),
|
||||||
'suppliers' => $this->supplierService->getAll(),
|
|
||||||
'filters' => $request->only(['supplier_id']),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
class RestockController extends Controller
|
class RestockController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private RestockService $service,
|
private readonly RestockService $service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
class TransactionController extends Controller
|
class TransactionController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private TransactionService $service,
|
private readonly TransactionService $service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
class CategoryController extends Controller
|
class CategoryController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CategoryService $service
|
private readonly CategoryService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
class CustomerController extends Controller
|
class CustomerController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CustomerService $service
|
private readonly CustomerService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -15,8 +15,8 @@
|
|||||||
class ProductController extends Controller
|
class ProductController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ProductService $service,
|
private readonly ProductService $service,
|
||||||
private CategoryService $categoryService,
|
private readonly CategoryService $categoryService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
class ProductVariantController extends Controller
|
class ProductVariantController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ProductVariantService $variantService,
|
private readonly ProductVariantService $variantService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function edit(Product $product, ProductVariant $variant): Response
|
public function edit(Product $product, ProductVariant $variant): Response
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
class StockMutationController extends Controller
|
class StockMutationController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private StockMutationService $service,
|
private readonly StockMutationService $service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(StockMutationRequest $request, Product $product, ProductVariant $variant): Response
|
public function index(StockMutationRequest $request, Product $product, ProductVariant $variant): Response
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
class RawMaterialController extends Controller
|
class RawMaterialController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private RawMaterialService $service,
|
private readonly RawMaterialService $service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
class RawMaterialVariantController extends Controller
|
class RawMaterialVariantController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private RawMaterialVariantService $variantService,
|
private readonly RawMaterialVariantService $variantService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function edit(RawMaterial $rawMaterial, RawMaterialPrice $variant): Response
|
public function edit(RawMaterial $rawMaterial, RawMaterialPrice $variant): Response
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
class SupplierController extends Controller
|
class SupplierController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private SupplierService $service
|
private readonly SupplierService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
class RoleController extends Controller
|
class RoleController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private RoleService $service
|
private readonly RoleService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
class PresignedUrlController extends Controller
|
class PresignedUrlController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $service
|
private readonly S3PresignedService $service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function store(PresignedUrlRequest $request): JsonResponse
|
public function store(PresignedUrlRequest $request): JsonResponse
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
class PushSubscriptionController extends Controller
|
class PushSubscriptionController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private PushSubscriptionService $service,
|
private readonly PushSubscriptionService $service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function store(StorePushSubscriptionRequest $request): JsonResponse
|
public function store(StorePushSubscriptionRequest $request): JsonResponse
|
||||||
|
|||||||
@ -16,16 +16,16 @@ public function rules(): array
|
|||||||
return [
|
return [
|
||||||
'description' => ['nullable', 'string', 'max:100'],
|
'description' => ['nullable', 'string', 'max:100'],
|
||||||
'product_name' => ['required', 'string', 'max:255'],
|
'product_name' => ['required', 'string', 'max:255'],
|
||||||
'sample' => ['required', 'integer'],
|
'sample' => ['required', 'integer', 'min:0'],
|
||||||
'original_outside_sample' => ['required', 'integer'],
|
'original_outside_sample' => ['required', 'integer', 'min:0'],
|
||||||
'cutting_result' => ['required', 'integer', 'min:1'],
|
'cutting_result' => ['required', 'integer', 'min:0'],
|
||||||
'materials' => ['required', 'array', 'min:1'],
|
'materials' => ['required', 'array', 'min:1'],
|
||||||
'materials.*.raw_material_price_id' => ['required', 'integer', 'exists:raw_material_prices,id'],
|
'materials.*.raw_material_price_id' => ['required', 'integer', 'exists:raw_material_prices,id'],
|
||||||
'materials.*.material_usage' => ['required', 'integer', 'min:1'],
|
'materials.*.material_usage' => ['required', 'integer', 'min:0'],
|
||||||
'materials.*.material_result' => ['required', 'integer'],
|
'materials.*.material_result' => ['required', 'integer', 'min:0'],
|
||||||
'materials.*.combination_index' => ['nullable', 'integer'],
|
'materials.*.combination_index' => ['nullable', 'integer', 'min:0'],
|
||||||
'combinations' => ['nullable', 'array'],
|
'combinations' => ['nullable', 'array'],
|
||||||
'combinations.*.material_result' => ['nullable', 'integer'],
|
'combinations.*.material_result' => ['nullable', 'integer', 'min:0'],
|
||||||
'photo_key' => ['nullable', 'string', 'max:500'],
|
'photo_key' => ['nullable', 'string', 'max:500'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ protected function formattedName(): Attribute
|
|||||||
protected function statusLabel(): Attribute
|
protected function statusLabel(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn () => $this->status?->label(),
|
get: fn () => $this->status->label(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,10 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Models\Scopes\ProductVariantScope;
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||||
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
|
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@ -17,7 +15,6 @@
|
|||||||
|
|
||||||
#[Appends(['formatted_name'])]
|
#[Appends(['formatted_name'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
#[ScopedBy([ProductVariantScope::class])]
|
|
||||||
class ProductVariant extends Model implements HasMedia
|
class ProductVariant extends Model implements HasMedia
|
||||||
{
|
{
|
||||||
use HasFactory, InteractsWithMedia, SoftDeletes;
|
use HasFactory, InteractsWithMedia, SoftDeletes;
|
||||||
|
|||||||
@ -5,15 +5,15 @@
|
|||||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class PushSubscription extends Model
|
class PushSubscription extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
public function user(): MorphTo
|
public function user(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->morphTo();
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,18 +41,6 @@ protected function unitLabel(): Attribute
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Scope]
|
|
||||||
protected function active(Builder $query): void
|
|
||||||
{
|
|
||||||
$query->where('is_active', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Scope]
|
|
||||||
protected function nonactive(Builder $query): void
|
|
||||||
{
|
|
||||||
$query->where('is_active', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Scope]
|
#[Scope]
|
||||||
protected function kg(Builder $query): void
|
protected function kg(Builder $query): void
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,11 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Models\Scopes\RawMaterialPriceScope;
|
|
||||||
use App\Services\S3PresignedService;
|
use App\Services\S3PresignedService;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||||
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
|
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@ -18,7 +16,6 @@
|
|||||||
|
|
||||||
#[Appends(['formatted_price'])]
|
#[Appends(['formatted_price'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
#[ScopedBy([RawMaterialPriceScope::class])]
|
|
||||||
class RawMaterialPrice extends Model implements HasMedia
|
class RawMaterialPrice extends Model implements HasMedia
|
||||||
{
|
{
|
||||||
use HasFactory, InteractsWithMedia, SoftDeletes;
|
use HasFactory, InteractsWithMedia, SoftDeletes;
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models\Scopes;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Scope;
|
|
||||||
|
|
||||||
class ProductVariantScope implements Scope
|
|
||||||
{
|
|
||||||
public function apply(Builder $builder, Model $model): void
|
|
||||||
{
|
|
||||||
$builder->orderBy('name');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models\Scopes;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Scope;
|
|
||||||
|
|
||||||
class RawMaterialPriceScope implements Scope
|
|
||||||
{
|
|
||||||
public function apply(Builder $builder, Model $model): void
|
|
||||||
{
|
|
||||||
$builder->orderBy('variant');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -181,6 +181,11 @@ public function purchaseItems(): HasMany
|
|||||||
return $this->hasMany(PurchaseItem::class);
|
return $this->hasMany(PurchaseItem::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function pushSubscriptions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(PushSubscription::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function rejections(): HasMany
|
public function rejections(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Rejection::class, 'rejected_by_id');
|
return $this->hasMany(Rejection::class, 'rejected_by_id');
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
class AdminSettingsService
|
class AdminSettingsService
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $s3Service,
|
private readonly S3PresignedService $s3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getSystemData(): array
|
public function getSystemData(): array
|
||||||
|
|||||||
@ -20,12 +20,12 @@ class CashAccountService
|
|||||||
use HandlesCashTransactions, RegistersMedia;
|
use HandlesCashTransactions, RegistersMedia;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $s3Service,
|
private readonly S3PresignedService $s3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function get(): ?CashAccount
|
public function get(): ?CashAccount
|
||||||
{
|
{
|
||||||
return CashAccount::select(['id', 'name', 'balance'])->first();
|
return CashAccount::select('id', 'name', 'balance')->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAllTransactions(array $filters = []): Collection
|
public function getAllTransactions(array $filters = []): Collection
|
||||||
@ -37,7 +37,7 @@ public function getAllTransactions(array $filters = []): Collection
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $cashAccount->cashTransactions()
|
return $cashAccount->cashTransactions()
|
||||||
->select(['id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at'])
|
->select('id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at')
|
||||||
->with('createdBy.userProfile', 'media')
|
->with('createdBy.userProfile', 'media')
|
||||||
->when($filters['type'] ?? null, function ($query, $type) {
|
->when($filters['type'] ?? null, function ($query, $type) {
|
||||||
$query->where('type', $type);
|
$query->where('type', $type);
|
||||||
@ -56,7 +56,7 @@ public function paginatedTransactions(int $perPage = 25, string $search = '', st
|
|||||||
}
|
}
|
||||||
|
|
||||||
$paginator = $cashAccount->cashTransactions()
|
$paginator = $cashAccount->cashTransactions()
|
||||||
->select(['id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at'])
|
->select('id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at')
|
||||||
->with('createdBy.userProfile', 'media')
|
->with('createdBy.userProfile', 'media')
|
||||||
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
||||||
->when($filters['type'] ?? null, function ($query, $type) {
|
->when($filters['type'] ?? null, function ($query, $type) {
|
||||||
|
|||||||
@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin\Finance;
|
namespace App\Services\Admin\Finance;
|
||||||
|
|
||||||
|
use App\Enums\CashTransactionType;
|
||||||
use App\Enums\EmployeeAdvanceStatus;
|
use App\Enums\EmployeeAdvanceStatus;
|
||||||
use App\Models\CashAccount;
|
use App\Models\CashAccount;
|
||||||
|
use App\Models\CashTransaction;
|
||||||
use App\Models\EmployeeAdvance;
|
use App\Models\EmployeeAdvance;
|
||||||
use App\Services\Concerns\HandlesCashTransactions;
|
use App\Services\Concerns\HandlesCashTransactions;
|
||||||
use App\Services\NotificationService;
|
use App\Services\NotificationService;
|
||||||
@ -15,10 +17,9 @@
|
|||||||
class EmployeeAdvanceService
|
class EmployeeAdvanceService
|
||||||
{
|
{
|
||||||
use HandlesCashTransactions;
|
use HandlesCashTransactions;
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
public function getAll(array $filters = []): Collection
|
||||||
{
|
{
|
||||||
return EmployeeAdvance::select(['id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at'])
|
return EmployeeAdvance::select('id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at')
|
||||||
->with(['employee.user.userProfile'])
|
->with(['employee.user.userProfile'])
|
||||||
->latest()
|
->latest()
|
||||||
->get();
|
->get();
|
||||||
@ -27,7 +28,7 @@ public function getAll(array $filters = []): Collection
|
|||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
return EmployeeAdvance::query()
|
return EmployeeAdvance::query()
|
||||||
->select(['id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at'])
|
->select('id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at')
|
||||||
->with(['employee.user.userProfile'])
|
->with(['employee.user.userProfile'])
|
||||||
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
||||||
->orderBy($sort, $direction)
|
->orderBy($sort, $direction)
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin\Finance;
|
namespace App\Services\Admin\Finance;
|
||||||
|
|
||||||
|
use App\Enums\CashTransactionType;
|
||||||
use App\Models\CashAccount;
|
use App\Models\CashAccount;
|
||||||
|
use App\Models\CashTransaction;
|
||||||
use App\Models\Expense;
|
use App\Models\Expense;
|
||||||
use App\Services\Concerns\HandlesCashTransactions;
|
use App\Services\Concerns\HandlesCashTransactions;
|
||||||
use App\Services\Concerns\RegistersMedia;
|
use App\Services\Concerns\RegistersMedia;
|
||||||
@ -19,12 +21,12 @@ class ExpenseService
|
|||||||
use HandlesCashTransactions, RegistersMedia;
|
use HandlesCashTransactions, RegistersMedia;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $s3Service,
|
private readonly S3PresignedService $s3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
public function getAll(array $filters = []): Collection
|
||||||
{
|
{
|
||||||
return Expense::select(['id', 'created_by_id', 'amount', 'description', 'created_at'])
|
return Expense::select('id', 'created_by_id', 'amount', 'description', 'created_at')
|
||||||
->with('createdBy.userProfile', 'media')
|
->with('createdBy.userProfile', 'media')
|
||||||
->latest()
|
->latest()
|
||||||
->get()
|
->get()
|
||||||
@ -34,7 +36,7 @@ public function getAll(array $filters = []): Collection
|
|||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$paginator = Expense::query()
|
$paginator = Expense::query()
|
||||||
->select(['id', 'created_by_id', 'amount', 'description', 'created_at'])
|
->select('id', 'created_by_id', 'amount', 'description', 'created_at')
|
||||||
->with('createdBy.userProfile', 'media')
|
->with('createdBy.userProfile', 'media')
|
||||||
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
||||||
->orderBy($sort, $direction)
|
->orderBy($sort, $direction)
|
||||||
|
|||||||
@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin\Finance;
|
namespace App\Services\Admin\Finance;
|
||||||
|
|
||||||
|
use App\Enums\CashTransactionType;
|
||||||
use App\Enums\PayrollPeriodStatus;
|
use App\Enums\PayrollPeriodStatus;
|
||||||
use App\Enums\PayrollStatus;
|
use App\Enums\PayrollStatus;
|
||||||
|
use App\Models\CashAccount;
|
||||||
|
use App\Models\CashTransaction;
|
||||||
use App\Models\Payroll;
|
use App\Models\Payroll;
|
||||||
use App\Models\PayrollPeriod;
|
use App\Models\PayrollPeriod;
|
||||||
use App\Services\Concerns\HandlesCashTransactions;
|
use App\Services\Concerns\HandlesCashTransactions;
|
||||||
@ -16,10 +19,9 @@
|
|||||||
class PayrollPeriodService
|
class PayrollPeriodService
|
||||||
{
|
{
|
||||||
use HandlesCashTransactions;
|
use HandlesCashTransactions;
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
public function getAll(array $filters = []): Collection
|
||||||
{
|
{
|
||||||
return PayrollPeriod::select(['id', 'year', 'month', 'status', 'closed_at', 'created_at'])
|
return PayrollPeriod::select('id', 'year', 'month', 'status', 'closed_at', 'created_at')
|
||||||
->withCount('payrolls')
|
->withCount('payrolls')
|
||||||
->withSum('payrolls', 'total_amount')
|
->withSum('payrolls', 'total_amount')
|
||||||
->withSum('payrolls', 'bonus_amount')
|
->withSum('payrolls', 'bonus_amount')
|
||||||
@ -38,7 +40,7 @@ public function getAll(array $filters = []): Collection
|
|||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
return PayrollPeriod::query()
|
return PayrollPeriod::query()
|
||||||
->select(['id', 'year', 'month', 'status', 'closed_at', 'created_at'])
|
->select('id', 'year', 'month', 'status', 'closed_at', 'created_at')
|
||||||
->withCount('payrolls')
|
->withCount('payrolls')
|
||||||
->withSum('payrolls', 'total_amount')
|
->withSum('payrolls', 'total_amount')
|
||||||
->withSum('payrolls', 'bonus_amount')
|
->withSum('payrolls', 'bonus_amount')
|
||||||
|
|||||||
@ -16,7 +16,7 @@ class AttendanceService
|
|||||||
use RegistersMedia;
|
use RegistersMedia;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $s3Service,
|
private readonly S3PresignedService $s3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getAll(): Collection
|
public function getAll(): Collection
|
||||||
|
|||||||
@ -14,9 +14,9 @@ public function getAll(array $filters = []): Collection
|
|||||||
return User::select(['id', 'email', 'username', 'is_active'])
|
return User::select(['id', 'email', 'username', 'is_active'])
|
||||||
->where(fn ($q) => $q->whereHas('employee')->orWhereHas('roles', fn ($rq) => $rq->where('name', 'Owner')))
|
->where(fn ($q) => $q->whereHas('employee')->orWhereHas('roles', fn ($rq) => $rq->where('name', 'Owner')))
|
||||||
->with([
|
->with([
|
||||||
'userProfile' => fn ($q) => $q->select(['id', 'user_id', 'full_name', 'phone_number', 'gender']),
|
'userProfile' => fn($q) => $q->select('id', 'user_id', 'full_name', 'phone_number', 'gender'),
|
||||||
'employee' => fn ($q) => $q->select(['id', 'user_id', 'join_date', 'employment_status', 'base_salary']),
|
'employee' => fn($q) => $q->select('id', 'user_id', 'join_date', 'employment_status', 'base_salary'),
|
||||||
'roles' => fn ($q) => $q->select(['id', 'name']),
|
'roles' => fn($q) => $q->select('id', 'name'),
|
||||||
])
|
])
|
||||||
->when($filters['employment_status'] ?? null, fn($q, $status) => $q->whereHas('employee', fn($eq) => $eq->where('employment_status', $status)))
|
->when($filters['employment_status'] ?? null, fn($q, $status) => $q->whereHas('employee', fn($eq) => $eq->where('employment_status', $status)))
|
||||||
->when(isset($filters['is_active']) && $filters['is_active'] !== '', fn($q) => $q->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN)))
|
->when(isset($filters['is_active']) && $filters['is_active'] !== '', fn($q) => $q->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN)))
|
||||||
@ -31,9 +31,9 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
->select(['id', 'email', 'username', 'is_active'])
|
->select(['id', 'email', 'username', 'is_active'])
|
||||||
->where(fn ($q) => $q->whereHas('employee')->orWhereHas('roles', fn ($rq) => $rq->where('name', 'Owner')))
|
->where(fn ($q) => $q->whereHas('employee')->orWhereHas('roles', fn ($rq) => $rq->where('name', 'Owner')))
|
||||||
->with([
|
->with([
|
||||||
'userProfile' => fn ($q) => $q->select(['id', 'user_id', 'full_name', 'phone_number', 'gender']),
|
'userProfile' => fn($q) => $q->select('id', 'user_id', 'full_name', 'phone_number', 'gender'),
|
||||||
'employee' => fn ($q) => $q->select(['id', 'user_id', 'join_date', 'employment_status', 'base_salary']),
|
'employee' => fn($q) => $q->select('id', 'user_id', 'join_date', 'employment_status', 'base_salary'),
|
||||||
'roles' => fn ($q) => $q->select(['id', 'name']),
|
'roles' => fn($q) => $q->select('id', 'name'),
|
||||||
])
|
])
|
||||||
->when($search, fn($q) => $q->whereHas('userProfile', fn($uq) => $uq->where('full_name', 'like', "%{$search}%")))
|
->when($search, fn($q) => $q->whereHas('userProfile', fn($uq) => $uq->where('full_name', 'like', "%{$search}%")))
|
||||||
->when($filters['employment_status'] ?? null, fn($q, $status) => $q->whereHas('employee', fn($eq) => $eq->where('employment_status', $status)))
|
->when($filters['employment_status'] ?? null, fn($q, $status) => $q->whereHas('employee', fn($eq) => $eq->where('employment_status', $status)))
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class LeaveRequestService
|
|||||||
{
|
{
|
||||||
public function getAll(array $filters = []): Collection
|
public function getAll(array $filters = []): Collection
|
||||||
{
|
{
|
||||||
return LeaveRequest::select(['id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at'])
|
return LeaveRequest::select('id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at')
|
||||||
->with(['employee.user.userProfile'])
|
->with(['employee.user.userProfile'])
|
||||||
->when($filters['status'] ?? null, function ($query, $status) {
|
->when($filters['status'] ?? null, function ($query, $status) {
|
||||||
$query->where('status', $status);
|
$query->where('status', $status);
|
||||||
@ -26,7 +26,7 @@ public function getAll(array $filters = []): Collection
|
|||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
return LeaveRequest::query()
|
return LeaveRequest::query()
|
||||||
->select(['id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at'])
|
->select('id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at')
|
||||||
->with(['employee.user.userProfile'])
|
->with(['employee.user.userProfile'])
|
||||||
->when($search, fn ($q) => $q->whereHas('employee.user.userProfile', fn ($uq) => $uq->where('full_name', 'like', "%{$search}%")))
|
->when($search, fn ($q) => $q->whereHas('employee.user.userProfile', fn ($uq) => $uq->where('full_name', 'like', "%{$search}%")))
|
||||||
->when($filters['status'] ?? null, function ($query, $status) {
|
->when($filters['status'] ?? null, function ($query, $status) {
|
||||||
|
|||||||
@ -19,13 +19,13 @@ class CuttingService
|
|||||||
use RegistersMedia;
|
use RegistersMedia;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $s3Service,
|
private readonly S3PresignedService $s3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$paginator = Cutting::query()
|
$paginator = Cutting::query()
|
||||||
->select(['id', 'created_by_id', 'status', 'description', 'total_material_cost', 'cost_per_unit', 'created_at'])
|
->select('id', 'created_by_id', 'status', 'description', 'total_material_cost', 'cost_per_unit', 'created_at')
|
||||||
->with([
|
->with([
|
||||||
'createdBy:id',
|
'createdBy:id',
|
||||||
'createdBy.userProfile:id,user_id,full_name',
|
'createdBy.userProfile:id,user_id,full_name',
|
||||||
@ -65,11 +65,10 @@ public function getForCreate(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'rawMaterials' => RawMaterial::query()
|
'rawMaterials' => RawMaterial::query()
|
||||||
->select(['id', 'name', 'unit', 'is_active'])
|
->select('id', 'name', 'unit', 'is_active')
|
||||||
->with([
|
->with([
|
||||||
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
||||||
])
|
])
|
||||||
->active()
|
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get()
|
->get()
|
||||||
->each(function (RawMaterial $rawMaterial) {
|
->each(function (RawMaterial $rawMaterial) {
|
||||||
@ -145,7 +144,6 @@ public function getForEdit(Cutting $cutting): array
|
|||||||
|
|
||||||
public function create(array $data): Cutting
|
public function create(array $data): Cutting
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($data) {
|
|
||||||
foreach ($data['materials'] as $materialData) {
|
foreach ($data['materials'] as $materialData) {
|
||||||
$usage = (int) ($materialData['material_usage'] ?? 0);
|
$usage = (int) ($materialData['material_usage'] ?? 0);
|
||||||
if ($usage <= 0) {
|
if ($usage <= 0) {
|
||||||
@ -162,6 +160,7 @@ public function create(array $data): Cutting
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($data) {
|
||||||
$cutting = Cutting::create([
|
$cutting = Cutting::create([
|
||||||
'created_by_id' => auth()->id(),
|
'created_by_id' => auth()->id(),
|
||||||
'status' => 'in_progress',
|
'status' => 'in_progress',
|
||||||
@ -247,7 +246,6 @@ public function create(array $data): Cutting
|
|||||||
|
|
||||||
public function update(Cutting $cutting, array $data): Cutting
|
public function update(Cutting $cutting, array $data): Cutting
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($cutting, $data) {
|
|
||||||
$cutting->load(['cuttingMaterials.rawMaterialPrice']);
|
$cutting->load(['cuttingMaterials.rawMaterialPrice']);
|
||||||
|
|
||||||
foreach ($cutting->cuttingMaterials as $oldMaterial) {
|
foreach ($cutting->cuttingMaterials as $oldMaterial) {
|
||||||
@ -272,6 +270,7 @@ public function update(Cutting $cutting, array $data): Cutting
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($cutting, $data) {
|
||||||
$cutting->load(['cuttingMaterials', 'cuttingMaterialCombinations', 'cuttingResults']);
|
$cutting->load(['cuttingMaterials', 'cuttingMaterialCombinations', 'cuttingResults']);
|
||||||
|
|
||||||
$cutting->cuttingResults()->delete();
|
$cutting->cuttingResults()->delete();
|
||||||
|
|||||||
@ -18,20 +18,18 @@ class PurchaseService
|
|||||||
use RegistersMedia;
|
use RegistersMedia;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $s3Service,
|
private readonly S3PresignedService $s3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$paginator = Purchase::query()
|
$paginator = Purchase::query()
|
||||||
->select(['id', 'supplier_id', 'created_by_id', 'subtotal', 'discount', 'shipping_cost', 'total', 'notes', 'created_at'])
|
->select('id', 'supplier_id', 'created_by_id', 'subtotal', 'discount', 'shipping_cost', 'total', 'notes', 'created_at')
|
||||||
->with([
|
->with([
|
||||||
'supplier:id,name',
|
'supplier:id,name',
|
||||||
'createdBy:id',
|
'createdBy:id',
|
||||||
'createdBy.userProfile:id,user_id,full_name',
|
'createdBy.userProfile:id,user_id,full_name',
|
||||||
'purchaseItems' => fn ($q) => $q
|
'purchaseItems:id,purchase_id,raw_material_price_id,quantity,unit_price,subtotal',
|
||||||
->select(['id', 'purchase_id', 'raw_material_price_id', 'quantity', 'unit_price', 'subtotal'])
|
|
||||||
->orderByRaw('(SELECT variant FROM raw_material_prices WHERE raw_material_prices.id = purchase_items.raw_material_price_id)'),
|
|
||||||
'purchaseItems.rawMaterialPrice:id,raw_material_id,variant,price,stock',
|
'purchaseItems.rawMaterialPrice:id,raw_material_id,variant,price,stock',
|
||||||
'purchaseItems.rawMaterialPrice.rawMaterial:id,name,unit',
|
'purchaseItems.rawMaterialPrice.rawMaterial:id,name,unit',
|
||||||
])
|
])
|
||||||
@ -39,7 +37,6 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
$q->whereHas('supplier', fn ($sq) => $sq->where('name', 'like', "%{$search}%"))
|
$q->whereHas('supplier', fn ($sq) => $sq->where('name', 'like', "%{$search}%"))
|
||||||
->orWhere('notes', 'like', "%{$search}%");
|
->orWhere('notes', 'like', "%{$search}%");
|
||||||
})
|
})
|
||||||
->when($filters['supplier_id'] ?? null, fn ($q, $supplierId) => $q->where('supplier_id', $supplierId))
|
|
||||||
->orderBy($sort, $direction)
|
->orderBy($sort, $direction)
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
|
|
||||||
@ -67,9 +64,9 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
public function getForCreate(): array
|
public function getForCreate(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'suppliers' => Supplier::select(['id', 'name'])->latest()->get(),
|
'suppliers' => Supplier::select('id', 'name')->latest()->get(),
|
||||||
'rawMaterials' => RawMaterial::query()
|
'rawMaterials' => RawMaterial::query()
|
||||||
->select(['id', 'name', 'unit', 'is_active'])
|
->select('id', 'name', 'unit', 'is_active')
|
||||||
->with([
|
->with([
|
||||||
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
||||||
])
|
])
|
||||||
@ -89,8 +86,6 @@ public function getForCreate(): array
|
|||||||
public function getForEdit(Purchase $purchase): array
|
public function getForEdit(Purchase $purchase): array
|
||||||
{
|
{
|
||||||
$purchase->load([
|
$purchase->load([
|
||||||
'purchaseItems' => fn ($q) => $q
|
|
||||||
->orderByRaw('(SELECT variant FROM raw_material_prices WHERE raw_material_prices.id = purchase_items.raw_material_price_id)'),
|
|
||||||
'purchaseItems.rawMaterialPrice.rawMaterial',
|
'purchaseItems.rawMaterialPrice.rawMaterial',
|
||||||
'supplier',
|
'supplier',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -20,19 +20,17 @@ class RestockService
|
|||||||
use HasStockAdjustment, RegistersMedia;
|
use HasStockAdjustment, RegistersMedia;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $s3Service,
|
private readonly S3PresignedService $s3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$paginator = Restock::query()
|
$paginator = Restock::query()
|
||||||
->select(['id', 'created_by_id', 'subtotal', 'total', 'notes', 'stock_type', 'created_at'])
|
->select('id', 'created_by_id', 'subtotal', 'total', 'notes', 'stock_type', 'created_at')
|
||||||
->with([
|
->with([
|
||||||
'createdBy:id',
|
'createdBy:id',
|
||||||
'createdBy.userProfile:id,user_id,full_name',
|
'createdBy.userProfile:id,user_id,full_name',
|
||||||
'restockItems' => fn ($q) => $q
|
'restockItems:id,restock_id,product_variant_id,quantity,unit_price,subtotal',
|
||||||
->select(['id', 'restock_id', 'product_variant_id', 'quantity', 'unit_price', 'subtotal'])
|
|
||||||
->orderByRaw('(SELECT name FROM product_variants WHERE product_variants.id = restock_items.product_variant_id)'),
|
|
||||||
'restockItems.productVariant:id,product_id,name,stock,reject_stock,retail_stock',
|
'restockItems.productVariant:id,product_id,name,stock,reject_stock,retail_stock',
|
||||||
'restockItems.productVariant.product:id,name',
|
'restockItems.productVariant.product:id,name',
|
||||||
])
|
])
|
||||||
@ -63,7 +61,7 @@ public function getForCreate(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'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',
|
||||||
'productVariants.productPrices:id,variant_id,type,price',
|
'productVariants.productPrices:id,variant_id,type,price',
|
||||||
@ -91,11 +89,7 @@ public function getForCreate(): array
|
|||||||
|
|
||||||
public function getForEdit(Restock $restock): array
|
public function getForEdit(Restock $restock): array
|
||||||
{
|
{
|
||||||
$restock->load([
|
$restock->load('restockItems.productVariant.product');
|
||||||
'restockItems' => fn ($q) => $q
|
|
||||||
->orderByRaw('(SELECT name FROM product_variants WHERE product_variants.id = restock_items.product_variant_id)'),
|
|
||||||
'restockItems.productVariant.product',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$media = $restock->getFirstMedia('photos');
|
$media = $restock->getFirstMedia('photos');
|
||||||
|
|
||||||
@ -241,4 +235,5 @@ private function buildItemRows(array $items, string $stockType, $now, int &$subt
|
|||||||
];
|
];
|
||||||
})->toArray();
|
})->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,13 +35,13 @@ class TransactionService
|
|||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $s3Service,
|
private readonly S3PresignedService $s3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$paginator = Order::query()
|
$paginator = Order::query()
|
||||||
->select(['id', 'created_by_id', 'customer_id', 'marketing_id', 'order_number', 'channel', 'price_type', 'status', 'payment_type', 'subtotal', 'discount', 'nego_price', 'total_amount', 'cogs', 'notes', 'created_at'])
|
->select('id', 'created_by_id', 'customer_id', 'marketing_id', 'order_number', 'channel', 'price_type', 'status', 'payment_type', 'subtotal', 'discount', 'nego_price', 'total_amount', 'cogs', 'notes', 'created_at')
|
||||||
->with([
|
->with([
|
||||||
'createdBy:id',
|
'createdBy:id',
|
||||||
'createdBy.userProfile:id,user_id,full_name',
|
'createdBy.userProfile:id,user_id,full_name',
|
||||||
@ -96,7 +96,7 @@ public function getFilterOptions(): array
|
|||||||
'channelOptions' => OrderChannel::toSelect(),
|
'channelOptions' => OrderChannel::toSelect(),
|
||||||
'paymentTypeOptions' => PaymentType::toSelect(),
|
'paymentTypeOptions' => PaymentType::toSelect(),
|
||||||
'customers' => Customer::query()
|
'customers' => Customer::query()
|
||||||
->select(['id', 'name'])
|
->select('id', 'name')
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get(),
|
->get(),
|
||||||
'employees' => User::query()
|
'employees' => User::query()
|
||||||
@ -118,7 +118,7 @@ public function getForCreate(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'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',
|
||||||
'productVariants.productPrices:id,variant_id,type,price',
|
'productVariants.productPrices:id,variant_id,type,price',
|
||||||
@ -137,7 +137,7 @@ public function getForCreate(): array
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
'customers' => Customer::query()
|
'customers' => Customer::query()
|
||||||
->select(['id', 'name'])
|
->select('id', 'name')
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get(),
|
->get(),
|
||||||
'employees' => User::query()
|
'employees' => User::query()
|
||||||
|
|||||||
@ -10,13 +10,13 @@ class CategoryService
|
|||||||
{
|
{
|
||||||
public function getAll(array $filters = []): Collection
|
public function getAll(array $filters = []): Collection
|
||||||
{
|
{
|
||||||
return Category::select(['id', 'name'])->latest()->get();
|
return Category::select('id', 'name')->latest()->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
return Category::query()
|
return Category::query()
|
||||||
->select(['id', 'name'])
|
->select('id', 'name')
|
||||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
||||||
->orderBy($sort, $direction)
|
->orderBy($sort, $direction)
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
|
|||||||
@ -10,13 +10,13 @@ class CustomerService
|
|||||||
{
|
{
|
||||||
public function getAll(array $filters = []): Collection
|
public function getAll(array $filters = []): Collection
|
||||||
{
|
{
|
||||||
return Customer::select(['id', 'name', 'phone_number', 'address'])->latest()->get();
|
return Customer::select('id', 'name', 'phone_number', 'address')->latest()->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
return Customer::query()
|
return Customer::query()
|
||||||
->select(['id', 'name', 'phone_number', 'address'])
|
->select('id', 'name', 'phone_number', 'address')
|
||||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
||||||
->orderBy($sort, $direction)
|
->orderBy($sort, $direction)
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
|
|||||||
@ -14,14 +14,14 @@
|
|||||||
class ProductService
|
class ProductService
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ProductVariantService $variantService,
|
private readonly ProductVariantService $variantService,
|
||||||
private S3PresignedService $s3Service,
|
private readonly S3PresignedService $s3Service,
|
||||||
private StockMutationService $stockMutationService,
|
private readonly StockMutationService $stockMutationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
public function getAll(array $filters = []): Collection
|
||||||
{
|
{
|
||||||
$products = Product::select(['id', 'name', 'slug', 'description', 'status'])
|
$products = Product::select('id', 'name', 'slug', 'description', 'status')
|
||||||
->with([
|
->with([
|
||||||
'categories:id,name',
|
'categories:id,name',
|
||||||
'productVariants:id,product_id,name,stock,reject_stock,retail_stock',
|
'productVariants:id,product_id,name,stock,reject_stock,retail_stock',
|
||||||
@ -46,7 +46,7 @@ public function getAll(array $filters = []): Collection
|
|||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$paginator = Product::query()
|
$paginator = Product::query()
|
||||||
->select(['id', 'name', 'slug', 'description', 'status'])
|
->select('id', 'name', 'slug', 'description', 'status')
|
||||||
->with([
|
->with([
|
||||||
'categories:id,name',
|
'categories:id,name',
|
||||||
'productVariants:id,product_id,name,stock,reject_stock,retail_stock',
|
'productVariants:id,product_id,name,stock,reject_stock,retail_stock',
|
||||||
|
|||||||
@ -16,8 +16,8 @@ class ProductVariantService
|
|||||||
use RegistersMedia;
|
use RegistersMedia;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $s3Service,
|
private readonly S3PresignedService $s3Service,
|
||||||
private StockMutationService $stockMutationService,
|
private readonly StockMutationService $stockMutationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getForEdit(ProductVariant $variant): array
|
public function getForEdit(ProductVariant $variant): array
|
||||||
|
|||||||
@ -15,12 +15,12 @@ class RawMaterialService
|
|||||||
use RegistersMedia;
|
use RegistersMedia;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $s3Service,
|
private readonly S3PresignedService $s3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
public function getAll(array $filters = []): Collection
|
||||||
{
|
{
|
||||||
return RawMaterial::select(['id', 'name', 'unit', 'is_active'])
|
return RawMaterial::select('id', 'name', 'unit', 'is_active')
|
||||||
->with([
|
->with([
|
||||||
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
||||||
'rawMaterialPrices.media',
|
'rawMaterialPrices.media',
|
||||||
@ -42,7 +42,7 @@ public function getAll(array $filters = []): Collection
|
|||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$paginator = RawMaterial::query()
|
$paginator = RawMaterial::query()
|
||||||
->select(['id', 'name', 'unit', 'is_active'])
|
->select('id', 'name', 'unit', 'is_active')
|
||||||
->with([
|
->with([
|
||||||
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
||||||
])
|
])
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class RawMaterialVariantService
|
|||||||
use RegistersMedia;
|
use RegistersMedia;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $s3Service,
|
private readonly S3PresignedService $s3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getForEdit(RawMaterialPrice $variant): array
|
public function getForEdit(RawMaterialPrice $variant): array
|
||||||
|
|||||||
@ -10,13 +10,13 @@ class SupplierService
|
|||||||
{
|
{
|
||||||
public function getAll(array $filters = []): Collection
|
public function getAll(array $filters = []): Collection
|
||||||
{
|
{
|
||||||
return Supplier::select(['id', 'name', 'phone_number', 'address'])->latest()->get();
|
return Supplier::select('id', 'name', 'phone_number', 'address')->latest()->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
return Supplier::query()
|
return Supplier::query()
|
||||||
->select(['id', 'name', 'phone_number', 'address'])
|
->select('id', 'name', 'phone_number', 'address')
|
||||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
||||||
->orderBy($sort, $direction)
|
->orderBy($sort, $direction)
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
use App\Enums\CashTransactionType;
|
use App\Enums\CashTransactionType;
|
||||||
use App\Models\CashAccount;
|
use App\Models\CashAccount;
|
||||||
use App\Models\CashTransaction;
|
use App\Models\CashTransaction;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
trait HandlesCashTransactions
|
trait HandlesCashTransactions
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
namespace App\Services\Concerns;
|
namespace App\Services\Concerns;
|
||||||
|
|
||||||
use App\Enums\ProductStockQuality;
|
use App\Enums\ProductStockQuality;
|
||||||
use App\Models\ProductVariant;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
trait HasStockAdjustment
|
trait HasStockAdjustment
|
||||||
@ -27,7 +26,7 @@ private function adjustVariantStock(int $variantId, int $quantity, int $sign, st
|
|||||||
$field = self::QUALITY_STOCK_MAP[$stockType] ?? 'stock';
|
$field = self::QUALITY_STOCK_MAP[$stockType] ?? 'stock';
|
||||||
|
|
||||||
$this->adjustStock(
|
$this->adjustStock(
|
||||||
model: app(ProductVariant::class)->newQuery()->findOrFail($variantId),
|
model: app(\App\Models\ProductVariant::class)->newQuery()->findOrFail($variantId),
|
||||||
field: $field,
|
field: $field,
|
||||||
quantity: $quantity,
|
quantity: $quantity,
|
||||||
sign: $sign,
|
sign: $sign,
|
||||||
|
|||||||
@ -241,6 +241,11 @@ public function run(): void
|
|||||||
'leave_requests.update',
|
'leave_requests.update',
|
||||||
'leave_requests.delete',
|
'leave_requests.delete',
|
||||||
|
|
||||||
|
'categories.view',
|
||||||
|
'categories.create',
|
||||||
|
'categories.update',
|
||||||
|
'categories.delete',
|
||||||
|
|
||||||
'suppliers.view',
|
'suppliers.view',
|
||||||
'suppliers.create',
|
'suppliers.create',
|
||||||
'suppliers.update',
|
'suppliers.update',
|
||||||
@ -254,6 +259,12 @@ public function run(): void
|
|||||||
|
|
||||||
'owner_verifications.view',
|
'owner_verifications.view',
|
||||||
|
|
||||||
|
'products.view',
|
||||||
|
'products.create',
|
||||||
|
'products.update',
|
||||||
|
'products.delete',
|
||||||
|
'products.toggle_status',
|
||||||
|
|
||||||
'cuttings.view',
|
'cuttings.view',
|
||||||
'cuttings.create',
|
'cuttings.create',
|
||||||
'cuttings.update',
|
'cuttings.update',
|
||||||
|
|||||||
@ -140,29 +140,6 @@ @layer base {
|
|||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
*::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb {
|
|
||||||
background-color: oklch(0.708 0 0 / 30%);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: oklch(0.708 0 0 / 50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: oklch(0.708 0 0 / 30%) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,3 @@
|
|||||||
import { Link, router } from '@inertiajs/react';
|
|
||||||
import type { LucideIcon } from 'lucide-react';
|
|
||||||
import {
|
|
||||||
Activity,
|
|
||||||
ArrowUpFromLine,
|
|
||||||
BarChart3,
|
|
||||||
Boxes,
|
|
||||||
CalendarCheck,
|
|
||||||
CalendarDays,
|
|
||||||
ClipboardCheck,
|
|
||||||
DollarSign,
|
|
||||||
HandCoins,
|
|
||||||
LayoutGrid,
|
|
||||||
Package,
|
|
||||||
RefreshCw,
|
|
||||||
Scissors,
|
|
||||||
Settings,
|
|
||||||
Shield,
|
|
||||||
ShoppingCart,
|
|
||||||
Tags,
|
|
||||||
Truck,
|
|
||||||
UserCircle,
|
|
||||||
Users,
|
|
||||||
Wallet,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import React from 'react';
|
|
||||||
import AppLogo from '@/components/app-logo';
|
import AppLogo from '@/components/app-logo';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
@ -56,6 +30,32 @@ import { index as productsIndex } from '@/routes/admin/master/products';
|
|||||||
import { index as rawMaterialsIndex } from '@/routes/admin/master/raw-materials';
|
import { index as rawMaterialsIndex } from '@/routes/admin/master/raw-materials';
|
||||||
import { index as suppliersIndex } from '@/routes/admin/master/suppliers';
|
import { index as suppliersIndex } from '@/routes/admin/master/suppliers';
|
||||||
import { index as rolesIndex } from '@/routes/admin/settings/roles';
|
import { index as rolesIndex } from '@/routes/admin/settings/roles';
|
||||||
|
import { Link, router } from '@inertiajs/react';
|
||||||
|
import type { LucideIcon } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Activity,
|
||||||
|
ArrowUpFromLine,
|
||||||
|
BarChart3,
|
||||||
|
Boxes,
|
||||||
|
CalendarCheck,
|
||||||
|
CalendarDays,
|
||||||
|
ClipboardCheck,
|
||||||
|
DollarSign,
|
||||||
|
HandCoins,
|
||||||
|
LayoutGrid,
|
||||||
|
Package,
|
||||||
|
RefreshCw,
|
||||||
|
Scissors,
|
||||||
|
Settings,
|
||||||
|
Shield,
|
||||||
|
ShoppingCart,
|
||||||
|
Tags,
|
||||||
|
Truck,
|
||||||
|
UserCircle,
|
||||||
|
Users,
|
||||||
|
Wallet,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
type NavMenuItem = { title: string; href: string; icon: LucideIcon; permission?: string | string[] };
|
type NavMenuItem = { title: string; href: string; icon: LucideIcon; permission?: string | string[] };
|
||||||
|
|
||||||
@ -111,20 +111,12 @@ function MenuGroup({ label, items }: { label: string; items: NavMenuItem[] }) {
|
|||||||
const { can, canAny } = useCan();
|
const { can, canAny } = useCan();
|
||||||
|
|
||||||
const filtered = items.filter((item) => {
|
const filtered = items.filter((item) => {
|
||||||
if (!item.permission) {
|
if (!item.permission) return true;
|
||||||
return true;
|
if (Array.isArray(item.permission)) return canAny(...item.permission);
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(item.permission)) {
|
|
||||||
return canAny(...item.permission);
|
|
||||||
}
|
|
||||||
|
|
||||||
return can(item.permission);
|
return can(item.permission);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
if (filtered.length === 0) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
|
|||||||
@ -53,7 +53,6 @@ interface CardTableProps<TData> {
|
|||||||
onPerPageChange?: (perPage: number) => void;
|
onPerPageChange?: (perPage: number) => void;
|
||||||
|
|
||||||
emptyText?: string;
|
emptyText?: string;
|
||||||
rowClassName?: (item: TData) => string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function useDebounce(callback: (value: string) => void, delay: number) {
|
function useDebounce(callback: (value: string) => void, delay: number) {
|
||||||
@ -88,7 +87,6 @@ export function CardTable<TData>({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
onPerPageChange,
|
onPerPageChange,
|
||||||
emptyText = 'Tidak ada data.',
|
emptyText = 'Tidak ada data.',
|
||||||
rowClassName,
|
|
||||||
}: CardTableProps<TData>) {
|
}: CardTableProps<TData>) {
|
||||||
const [localSearch, setLocalSearch] = React.useState(searchValue ?? '');
|
const [localSearch, setLocalSearch] = React.useState(searchValue ?? '');
|
||||||
|
|
||||||
@ -178,7 +176,7 @@ return true;
|
|||||||
const isExpanded = isItemExpanded(key);
|
const isExpanded = isItemExpanded(key);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} className={`flex flex-col ${rowClassName?.(item) ?? ''}`}>
|
<div key={key} className="flex flex-col">
|
||||||
{renderCard({
|
{renderCard({
|
||||||
item,
|
item,
|
||||||
index,
|
index,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Clock, LogIn, LogOut } from 'lucide-react';
|
import { Clock, LogIn, LogOut } from 'lucide-react';
|
||||||
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 { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
type TodayAttendance = {
|
type TodayAttendance = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -29,12 +29,8 @@ export function AttendanceCard({ todayAttendance, isOnLeave, canCheckIn, onCheck
|
|||||||
const hasCheckedOut = !!todayAttendance?.check_out_at;
|
const hasCheckedOut = !!todayAttendance?.check_out_at;
|
||||||
|
|
||||||
function formatTime(dateStr: string | null): string {
|
function formatTime(dateStr: string | null): string {
|
||||||
if (!dateStr) {
|
if (!dateStr) return '-';
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
const d = new Date(dateStr);
|
const d = new Date(dateStr);
|
||||||
|
|
||||||
return d.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', hour12: false });
|
return d.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,14 +6,8 @@ export function useCardTableExpand(
|
|||||||
defaultExpanded: boolean | (number | string)[] = false,
|
defaultExpanded: boolean | (number | string)[] = false,
|
||||||
) {
|
) {
|
||||||
const [expandedKeys, setExpandedKeys] = useState<ExpandState>(() => {
|
const [expandedKeys, setExpandedKeys] = useState<ExpandState>(() => {
|
||||||
if (defaultExpanded === true) {
|
if (defaultExpanded === true) return 'all';
|
||||||
return 'all';
|
if (Array.isArray(defaultExpanded)) return new Set(defaultExpanded);
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(defaultExpanded)) {
|
|
||||||
return new Set(defaultExpanded);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Set();
|
return new Set();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -22,15 +16,12 @@ return new Set(defaultExpanded);
|
|||||||
if (prev === 'all') {
|
if (prev === 'all') {
|
||||||
return new Set([key]);
|
return new Set([key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
|
|
||||||
if (next.has(key)) {
|
if (next.has(key)) {
|
||||||
next.delete(key);
|
next.delete(key);
|
||||||
} else {
|
} else {
|
||||||
next.add(key);
|
next.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
@ -45,10 +36,7 @@ return new Set(defaultExpanded);
|
|||||||
|
|
||||||
const isExpanded = useCallback(
|
const isExpanded = useCallback(
|
||||||
(key: number | string): boolean => {
|
(key: number | string): boolean => {
|
||||||
if (expandedKeys === 'all') {
|
if (expandedKeys === 'all') return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return expandedKeys.has(key);
|
return expandedKeys.has(key);
|
||||||
},
|
},
|
||||||
[expandedKeys],
|
[expandedKeys],
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { router } from '@inertiajs/react';
|
|
||||||
import { Bell, Check, Trash2 } from 'lucide-react';
|
import { Bell, Check, Trash2 } from 'lucide-react';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@ -190,11 +189,15 @@ export function NotificationBell() {
|
|||||||
: 'opacity-60'
|
: 'opacity-60'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<a
|
||||||
|
href={notification.url || '#'}
|
||||||
className="min-w-0 flex-1 cursor-pointer"
|
className="min-w-0 flex-1 cursor-pointer"
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
if (notification.url) {
|
if (notification.url) {
|
||||||
router.visit(notification.url);
|
window.location.href =
|
||||||
|
notification.url;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -222,7 +225,7 @@ export function NotificationBell() {
|
|||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</a>
|
||||||
<div className="flex shrink-0 flex-col gap-1 pt-0.5">
|
<div className="flex shrink-0 flex-col gap-1 pt-0.5">
|
||||||
{!notification.is_read && (
|
{!notification.is_read && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -17,10 +17,7 @@ type PageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function extractNames(items?: RoleOrPermission[]): string[] {
|
function extractNames(items?: RoleOrPermission[]): string[] {
|
||||||
if (!items) {
|
if (!items) return [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return items.map((item) => (typeof item === 'string' ? item : item.name));
|
return items.map((item) => (typeof item === 'string' ? item : item.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,42 +29,24 @@ export function useCan() {
|
|||||||
const permissionNames = extractNames(user?.permissions);
|
const permissionNames = extractNames(user?.permissions);
|
||||||
|
|
||||||
function can(permission: string): boolean {
|
function can(permission: string): boolean {
|
||||||
if (!user) {
|
if (!user) return false;
|
||||||
return false;
|
if (roleNames.includes('developer') || roleNames.includes('owner')) return true;
|
||||||
}
|
|
||||||
|
|
||||||
if (roleNames.includes('developer') || roleNames.includes('owner')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return permissionNames.includes(permission);
|
return permissionNames.includes(permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
function canAny(...permissions: string[]): boolean {
|
function canAny(...permissions: string[]): boolean {
|
||||||
if (!user) {
|
if (!user) return false;
|
||||||
return false;
|
if (roleNames.includes('developer') || roleNames.includes('owner')) return true;
|
||||||
}
|
|
||||||
|
|
||||||
if (roleNames.includes('developer') || roleNames.includes('owner')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return permissions.some((p) => permissionNames.includes(p));
|
return permissions.some((p) => permissionNames.includes(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasRole(role: string): boolean {
|
function hasRole(role: string): boolean {
|
||||||
if (!user) {
|
if (!user) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return roleNames.includes(role);
|
return roleNames.includes(role);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasAnyRole(roles: string[]): boolean {
|
function hasAnyRole(roles: string[]): boolean {
|
||||||
if (!user) {
|
if (!user) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return roles.some((role) => roleNames.includes(role));
|
return roles.some((role) => roleNames.includes(role));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,15 +6,12 @@ export function formatRupiahShort(value: number): string {
|
|||||||
if (value >= 1_000_000_000) {
|
if (value >= 1_000_000_000) {
|
||||||
return (value / 1_000_000_000).toFixed(1).replace('.0', '') + 'jt';
|
return (value / 1_000_000_000).toFixed(1).replace('.0', '') + 'jt';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value >= 1_000_000) {
|
if (value >= 1_000_000) {
|
||||||
return (value / 1_000_000).toFixed(1).replace('.0', '') + 'jt';
|
return (value / 1_000_000).toFixed(1).replace('.0', '') + 'jt';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value >= 1_000) {
|
if (value >= 1_000) {
|
||||||
return (value / 1_000).toFixed(0) + 'rb';
|
return (value / 1_000).toFixed(0) + 'rb';
|
||||||
}
|
}
|
||||||
|
|
||||||
return value.toString();
|
return value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,13 +12,12 @@ export type CashAccount = {
|
|||||||
type CreateColumnsParams = {
|
type CreateColumnsParams = {
|
||||||
handleEdit: (cashAccount: CashAccount) => void;
|
handleEdit: (cashAccount: CashAccount) => void;
|
||||||
handleDeleteClick: (cashAccount: CashAccount) => void;
|
handleDeleteClick: (cashAccount: CashAccount) => void;
|
||||||
can: (permission: string) => boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createCashAccountColumns(
|
export function createCashAccountColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<CashAccount>[] {
|
): ColumnDef<CashAccount>[] {
|
||||||
const { handleEdit, handleDeleteClick, can } = params;
|
const { handleEdit, handleDeleteClick } = params;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -55,7 +54,6 @@ export function createCashAccountColumns(
|
|||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show: can('cash.update'),
|
|
||||||
onClick: () => handleEdit(cashAccount),
|
onClick: () => handleEdit(cashAccount),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -63,7 +61,6 @@ export function createCashAccountColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show: can('cash.delete'),
|
|
||||||
onClick: () => handleDeleteClick(cashAccount),
|
onClick: () => handleDeleteClick(cashAccount),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
@ -70,7 +69,6 @@ export default function CashAccountIndex({
|
|||||||
filters,
|
filters,
|
||||||
filterOptions,
|
filterOptions,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { can } = useCan();
|
|
||||||
const [depositOpen, setDepositOpen] = useState(false);
|
const [depositOpen, setDepositOpen] = useState(false);
|
||||||
const [withdrawalOpen, setWithdrawalOpen] = useState(false);
|
const [withdrawalOpen, setWithdrawalOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<CashTransaction | null>(null);
|
const [editing, setEditing] = useState<CashTransaction | null>(null);
|
||||||
@ -139,7 +137,6 @@ export default function CashAccountIndex({
|
|||||||
setEditReceiptKey(transaction.receipt_key ?? null);
|
setEditReceiptKey(transaction.receipt_key ?? null);
|
||||||
},
|
},
|
||||||
handleDeleteClick: (transaction) => setDeleting(transaction),
|
handleDeleteClick: (transaction) => setDeleting(transaction),
|
||||||
can,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterToolbar = (
|
const filterToolbar = (
|
||||||
@ -183,7 +180,6 @@ export default function CashAccountIndex({
|
|||||||
title="Kas Toko"
|
title="Kas Toko"
|
||||||
actions={
|
actions={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{can('cash.deposit') && (
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setDepositOpen(true)}
|
onClick={() => setDepositOpen(true)}
|
||||||
@ -191,8 +187,6 @@ export default function CashAccountIndex({
|
|||||||
<ArrowDownToLine className="h-4 w-4" />
|
<ArrowDownToLine className="h-4 w-4" />
|
||||||
Deposit
|
Deposit
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
{can('cash.withdraw') && (
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setWithdrawalOpen(true)}
|
onClick={() => setWithdrawalOpen(true)}
|
||||||
@ -200,7 +194,6 @@ export default function CashAccountIndex({
|
|||||||
<ArrowUpFromLine className="h-4 w-4" />
|
<ArrowUpFromLine className="h-4 w-4" />
|
||||||
Withdrawal
|
Withdrawal
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -50,13 +50,12 @@ function getReferenceLabel(type: string): string {
|
|||||||
type CreateColumnsParams = {
|
type CreateColumnsParams = {
|
||||||
handleEdit: (transaction: CashTransaction) => void;
|
handleEdit: (transaction: CashTransaction) => void;
|
||||||
handleDeleteClick: (transaction: CashTransaction) => void;
|
handleDeleteClick: (transaction: CashTransaction) => void;
|
||||||
can: (permission: string) => boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createTransactionColumns(
|
export function createTransactionColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<CashTransaction>[] {
|
): ColumnDef<CashTransaction>[] {
|
||||||
const { handleEdit, handleDeleteClick, can } = params;
|
const { handleEdit, handleDeleteClick } = params;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -175,7 +174,6 @@ export function createTransactionColumns(
|
|||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show: can('cash.update'),
|
|
||||||
onClick: () => handleEdit(transaction),
|
onClick: () => handleEdit(transaction),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -183,7 +181,6 @@ export function createTransactionColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show: can('cash.delete'),
|
|
||||||
onClick: () => handleDeleteClick(transaction),
|
onClick: () => handleDeleteClick(transaction),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -62,14 +62,12 @@ type CreateColumnsParams = {
|
|||||||
handleDeleteClick: (employeeAdvance: EmployeeAdvance) => void;
|
handleDeleteClick: (employeeAdvance: EmployeeAdvance) => void;
|
||||||
handleApprove: (employeeAdvance: EmployeeAdvance) => void;
|
handleApprove: (employeeAdvance: EmployeeAdvance) => void;
|
||||||
handlePay: (employeeAdvance: EmployeeAdvance) => void;
|
handlePay: (employeeAdvance: EmployeeAdvance) => void;
|
||||||
can: (permission: string) => boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createEmployeeAdvanceColumns(
|
export function createEmployeeAdvanceColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<EmployeeAdvance>[] {
|
): ColumnDef<EmployeeAdvance>[] {
|
||||||
const { handleEdit, handleDeleteClick, handleApprove, handlePay, can } =
|
const { handleEdit, handleDeleteClick, handleApprove, handlePay } = params;
|
||||||
params;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -144,9 +142,7 @@ export function createEmployeeAdvanceColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||||
),
|
),
|
||||||
show:
|
show: employeeAdvance.status === 'pending',
|
||||||
can('employee_advances.verify') &&
|
|
||||||
employeeAdvance.status === 'pending',
|
|
||||||
onClick: () => handleApprove(employeeAdvance),
|
onClick: () => handleApprove(employeeAdvance),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -154,15 +150,12 @@ export function createEmployeeAdvanceColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<CircleDollarSign className="h-4 w-4 text-blue-600" />
|
<CircleDollarSign className="h-4 w-4 text-blue-600" />
|
||||||
),
|
),
|
||||||
show:
|
show: employeeAdvance.status === 'approved',
|
||||||
can('employee_advances.pay') &&
|
|
||||||
employeeAdvance.status === 'approved',
|
|
||||||
onClick: () => handlePay(employeeAdvance),
|
onClick: () => handlePay(employeeAdvance),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show: can('employee_advances.update'),
|
|
||||||
onClick: () => handleEdit(employeeAdvance),
|
onClick: () => handleEdit(employeeAdvance),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -170,7 +163,6 @@ export function createEmployeeAdvanceColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show: can('employee_advances.delete'),
|
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
handleDeleteClick(employeeAdvance),
|
handleDeleteClick(employeeAdvance),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import { RupiahInput } from '@/components/rupiah-input';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import {
|
import {
|
||||||
destroy,
|
destroy,
|
||||||
@ -36,7 +35,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) {
|
export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) {
|
||||||
const { can } = useCan();
|
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<EmployeeAdvance | null>(null);
|
const [editing, setEditing] = useState<EmployeeAdvance | null>(null);
|
||||||
const [deleting, setDeleting] = useState<EmployeeAdvance | null>(null);
|
const [deleting, setDeleting] = useState<EmployeeAdvance | null>(null);
|
||||||
@ -115,7 +113,6 @@ export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) {
|
|||||||
handleDeleteClick: (employeeAdvance) => setDeleting(employeeAdvance),
|
handleDeleteClick: (employeeAdvance) => setDeleting(employeeAdvance),
|
||||||
handleApprove: (employeeAdvance) => setApproving(employeeAdvance),
|
handleApprove: (employeeAdvance) => setApproving(employeeAdvance),
|
||||||
handlePay: (employeeAdvance) => setPaying(employeeAdvance),
|
handlePay: (employeeAdvance) => setPaying(employeeAdvance),
|
||||||
can,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -126,7 +123,6 @@ export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Kasbon"
|
title="Kasbon"
|
||||||
actions={
|
actions={
|
||||||
can('employee_advances.create') ? (
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -136,7 +132,6 @@ export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) {
|
|||||||
Tambah
|
Tambah
|
||||||
</button>
|
</button>
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -22,13 +22,12 @@ export type Expense = {
|
|||||||
type CreateColumnsParams = {
|
type CreateColumnsParams = {
|
||||||
handleEdit: (expense: Expense) => void;
|
handleEdit: (expense: Expense) => void;
|
||||||
handleDeleteClick: (expense: Expense) => void;
|
handleDeleteClick: (expense: Expense) => void;
|
||||||
can: (permission: string) => boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createExpenseColumns(
|
export function createExpenseColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<Expense>[] {
|
): ColumnDef<Expense>[] {
|
||||||
const { handleEdit, handleDeleteClick, can } = params;
|
const { handleEdit, handleDeleteClick } = params;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -99,7 +98,6 @@ export function createExpenseColumns(
|
|||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show: can('expenses.update'),
|
|
||||||
onClick: () => handleEdit(expense),
|
onClick: () => handleEdit(expense),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -107,7 +105,6 @@ export function createExpenseColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show: can('expenses.delete'),
|
|
||||||
onClick: () => handleDeleteClick(expense),
|
onClick: () => handleDeleteClick(expense),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import { RupiahInput } from '@/components/rupiah-input';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import {
|
import {
|
||||||
destroy,
|
destroy,
|
||||||
@ -34,7 +33,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function ExpenseIndex({ expenses }: Props) {
|
export default function ExpenseIndex({ expenses }: Props) {
|
||||||
const { can } = useCan();
|
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<Expense | null>(null);
|
const [editing, setEditing] = useState<Expense | null>(null);
|
||||||
const [deleting, setDeleting] = useState<Expense | null>(null);
|
const [deleting, setDeleting] = useState<Expense | null>(null);
|
||||||
@ -86,7 +84,6 @@ export default function ExpenseIndex({ expenses }: Props) {
|
|||||||
setEditReceiptKey(expense.receipt_key ?? null);
|
setEditReceiptKey(expense.receipt_key ?? null);
|
||||||
},
|
},
|
||||||
handleDeleteClick: (expense) => setDeleting(expense),
|
handleDeleteClick: (expense) => setDeleting(expense),
|
||||||
can,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -97,7 +94,6 @@ export default function ExpenseIndex({ expenses }: Props) {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Pengeluaran"
|
title="Pengeluaran"
|
||||||
actions={
|
actions={
|
||||||
can('expenses.create') ? (
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -107,7 +103,6 @@ export default function ExpenseIndex({ expenses }: Props) {
|
|||||||
Tambah
|
Tambah
|
||||||
</button>
|
</button>
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -49,13 +49,12 @@ type CreateColumnsParams = {
|
|||||||
showUrl: (id: number) => string;
|
showUrl: (id: number) => string;
|
||||||
handleClose: (period: PayrollPeriod) => void;
|
handleClose: (period: PayrollPeriod) => void;
|
||||||
handleReopen: (period: PayrollPeriod) => void;
|
handleReopen: (period: PayrollPeriod) => void;
|
||||||
can: (permission: string) => boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createPayrollPeriodColumns(
|
export function createPayrollPeriodColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<PayrollPeriod>[] {
|
): ColumnDef<PayrollPeriod>[] {
|
||||||
const { showUrl, handleClose, handleReopen, can } = params;
|
const { showUrl, handleClose, handleReopen } = params;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -192,9 +191,7 @@ export function createPayrollPeriodColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<Lock className="h-4 w-4 text-orange-600" />
|
<Lock className="h-4 w-4 text-orange-600" />
|
||||||
),
|
),
|
||||||
show:
|
show: period.status === 'open',
|
||||||
can('payroll.adjust') &&
|
|
||||||
period.status === 'open',
|
|
||||||
onClick: () => handleClose(period),
|
onClick: () => handleClose(period),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -202,9 +199,7 @@ export function createPayrollPeriodColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<Unlock className="h-4 w-4 text-blue-600" />
|
<Unlock className="h-4 w-4 text-blue-600" />
|
||||||
),
|
),
|
||||||
show:
|
show: period.status === 'closed',
|
||||||
can('payroll.adjust') &&
|
|
||||||
period.status === 'closed',
|
|
||||||
onClick: () => handleReopen(period),
|
onClick: () => handleReopen(period),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import type { PaginationState } from '@/components/data-table';
|
|||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import { MONTH_NAMES } from '@/lib/constants';
|
import { MONTH_NAMES } from '@/lib/constants';
|
||||||
import {
|
import {
|
||||||
@ -27,7 +26,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function PayrollPeriodIndex({ payrollPeriods }: Props) {
|
export default function PayrollPeriodIndex({ payrollPeriods }: Props) {
|
||||||
const { can } = useCan();
|
|
||||||
const [closing, setClosing] = useState<PayrollPeriod | null>(null);
|
const [closing, setClosing] = useState<PayrollPeriod | null>(null);
|
||||||
const [reopening, setReopening] = useState<PayrollPeriod | null>(null);
|
const [reopening, setReopening] = useState<PayrollPeriod | null>(null);
|
||||||
|
|
||||||
@ -80,7 +78,6 @@ export default function PayrollPeriodIndex({ payrollPeriods }: Props) {
|
|||||||
showUrl: (id) => payrollPeriodShow(id).url,
|
showUrl: (id) => payrollPeriodShow(id).url,
|
||||||
handleClose: (period) => setClosing(period),
|
handleClose: (period) => setClosing(period),
|
||||||
handleReopen: (period) => setReopening(period),
|
handleReopen: (period) => setReopening(period),
|
||||||
can,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -67,7 +67,6 @@ type CreateColumnsParams = {
|
|||||||
adjustment: PayrollAdjustment,
|
adjustment: PayrollAdjustment,
|
||||||
payrollId: number,
|
payrollId: number,
|
||||||
) => void;
|
) => void;
|
||||||
can: (permission: string) => boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createPayrollColumns(
|
export function createPayrollColumns(
|
||||||
@ -78,7 +77,6 @@ export function createPayrollColumns(
|
|||||||
handleCancel,
|
handleCancel,
|
||||||
handleAddAdjustment,
|
handleAddAdjustment,
|
||||||
handleDeleteAdjustment,
|
handleDeleteAdjustment,
|
||||||
can,
|
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -220,9 +218,7 @@ export function createPayrollColumns(
|
|||||||
{
|
{
|
||||||
label: 'Tambah Adjustment',
|
label: 'Tambah Adjustment',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show:
|
show: payroll.status === 'unpaid',
|
||||||
can('payroll.adjust') &&
|
|
||||||
payroll.status === 'unpaid',
|
|
||||||
onClick: () => handleAddAdjustment(payroll),
|
onClick: () => handleAddAdjustment(payroll),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -230,9 +226,7 @@ export function createPayrollColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<CircleDollarSign className="h-4 w-4 text-green-600" />
|
<CircleDollarSign className="h-4 w-4 text-green-600" />
|
||||||
),
|
),
|
||||||
show:
|
show: payroll.status === 'unpaid',
|
||||||
can('payroll.pay') &&
|
|
||||||
payroll.status === 'unpaid',
|
|
||||||
onClick: () => handlePay(payroll),
|
onClick: () => handlePay(payroll),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -240,9 +234,7 @@ export function createPayrollColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<XCircle className="h-4 w-4 text-destructive" />
|
<XCircle className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show:
|
show: payroll.status === 'unpaid',
|
||||||
can('payroll.cancel') &&
|
|
||||||
payroll.status === 'unpaid',
|
|
||||||
onClick: () => handleCancel(payroll),
|
onClick: () => handleCancel(payroll),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Form, Head, Link, router } from '@inertiajs/react';
|
import { Form, Head, router } from '@inertiajs/react';
|
||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
@ -16,7 +16,6 @@ import {
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { MONTH_NAMES } from '@/lib/constants';
|
import { MONTH_NAMES } from '@/lib/constants';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import { destroy as adjustmentDestroy } from '@/routes/admin/finance/payroll-adjustments';
|
import { destroy as adjustmentDestroy } from '@/routes/admin/finance/payroll-adjustments';
|
||||||
@ -40,7 +39,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function PayrollPeriodShow({ payrollPeriod }: Props) {
|
export default function PayrollPeriodShow({ payrollPeriod }: Props) {
|
||||||
const { can } = useCan();
|
|
||||||
const [paying, setPaying] = useState<Payroll | null>(null);
|
const [paying, setPaying] = useState<Payroll | null>(null);
|
||||||
const [cancelling, setCancelling] = useState<Payroll | null>(null);
|
const [cancelling, setCancelling] = useState<Payroll | null>(null);
|
||||||
const [addingAdjustment, setAddingAdjustment] = useState<Payroll | null>(
|
const [addingAdjustment, setAddingAdjustment] = useState<Payroll | null>(
|
||||||
@ -100,7 +98,6 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) {
|
|||||||
handleDeleteAdjustment: (adjustment, payrollId) => {
|
handleDeleteAdjustment: (adjustment, payrollId) => {
|
||||||
setDeletingAdjustment({ adjustment, payrollId });
|
setDeletingAdjustment({ adjustment, payrollId });
|
||||||
},
|
},
|
||||||
can,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalBaseSalary = payrollPeriod.payrolls.reduce(
|
const totalBaseSalary = payrollPeriod.payrolls.reduce(
|
||||||
@ -142,10 +139,10 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={payrollPeriodsIndex.url()}>
|
<a href={payrollPeriodsIndex.url()}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Kembali
|
Kembali
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,6 @@ type CreateColumnsParams = {
|
|||||||
handleDeleteClick: (employee: Employee) => void;
|
handleDeleteClick: (employee: Employee) => void;
|
||||||
handleResetPassword: (employee: Employee) => void;
|
handleResetPassword: (employee: Employee) => void;
|
||||||
toggleActiveUrl: (id: number) => string;
|
toggleActiveUrl: (id: number) => string;
|
||||||
can: (permission: string) => boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createEmployeeColumns(
|
export function createEmployeeColumns(
|
||||||
@ -49,7 +48,6 @@ export function createEmployeeColumns(
|
|||||||
handleDeleteClick,
|
handleDeleteClick,
|
||||||
handleResetPassword,
|
handleResetPassword,
|
||||||
toggleActiveUrl,
|
toggleActiveUrl,
|
||||||
can,
|
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -163,7 +161,6 @@ export function createEmployeeColumns(
|
|||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show: can('employees.update'),
|
|
||||||
onClick: () => handleEdit(employee),
|
onClick: () => handleEdit(employee),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -171,7 +168,6 @@ export function createEmployeeColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<KeyRound className="h-4 w-4 text-muted-foreground" />
|
<KeyRound className="h-4 w-4 text-muted-foreground" />
|
||||||
),
|
),
|
||||||
show: can('employees.reset_password'),
|
|
||||||
onClick: () => handleResetPassword(employee),
|
onClick: () => handleResetPassword(employee),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -179,7 +175,6 @@ export function createEmployeeColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show: can('employees.delete'),
|
|
||||||
onClick: () => handleDeleteClick(employee),
|
onClick: () => handleDeleteClick(employee),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Form, Head, Link } from '@inertiajs/react';
|
import { Form, Head } from '@inertiajs/react';
|
||||||
import { AlertCircle, ArrowLeft } from 'lucide-react';
|
import { AlertCircle, ArrowLeft } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { DatePicker } from '@/components/date-picker';
|
import { DatePicker } from '@/components/date-picker';
|
||||||
@ -55,10 +55,10 @@ export default function EmployeeCreate({ roles }: Props) {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={employeeIndex.url()}>
|
<a href={employeeIndex.url()}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Kembali
|
Kembali
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Form, Head, Link } from '@inertiajs/react';
|
import { Form, Head } from '@inertiajs/react';
|
||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { DatePicker } from '@/components/date-picker';
|
import { DatePicker } from '@/components/date-picker';
|
||||||
@ -94,10 +94,10 @@ export default function EmployeeEdit({ employee, roles }: Props) {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={employeeIndex.url()}>
|
<a href={employeeIndex.url()}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Kembali
|
Kembali
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Head, Link, router } from '@inertiajs/react';
|
import { Head, router } from '@inertiajs/react';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { PaginationState } from '@/components/data-table';
|
import type { PaginationState } from '@/components/data-table';
|
||||||
@ -14,7 +14,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import {
|
import {
|
||||||
destroy,
|
destroy,
|
||||||
@ -43,7 +42,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function EmployeeIndex({ employees, filters }: Props) {
|
export default function EmployeeIndex({ employees, filters }: Props) {
|
||||||
const { can } = useCan();
|
|
||||||
const [deleting, setDeleting] = useState<Employee | null>(null);
|
const [deleting, setDeleting] = useState<Employee | null>(null);
|
||||||
const [resetPasswordTarget, setResetPasswordTarget] =
|
const [resetPasswordTarget, setResetPasswordTarget] =
|
||||||
useState<Employee | null>(null);
|
useState<Employee | null>(null);
|
||||||
@ -96,12 +94,11 @@ export default function EmployeeIndex({ employees, filters }: Props) {
|
|||||||
|
|
||||||
const columns = createEmployeeColumns({
|
const columns = createEmployeeColumns({
|
||||||
handleEdit: (employee) => {
|
handleEdit: (employee) => {
|
||||||
router.visit(employeeEdit.url(employee.id));
|
window.location.href = employeeEdit.url(employee.id);
|
||||||
},
|
},
|
||||||
handleDeleteClick: (employee) => setDeleting(employee),
|
handleDeleteClick: (employee) => setDeleting(employee),
|
||||||
handleResetPassword: (employee) => setResetPasswordTarget(employee),
|
handleResetPassword: (employee) => setResetPasswordTarget(employee),
|
||||||
toggleActiveUrl: (id) => toggleActive.url(id),
|
toggleActiveUrl: (id) => toggleActive.url(id),
|
||||||
can,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterToolbar = (
|
const filterToolbar = (
|
||||||
@ -186,14 +183,12 @@ export default function EmployeeIndex({ employees, filters }: Props) {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Pegawai"
|
title="Pegawai"
|
||||||
actions={
|
actions={
|
||||||
can('employees.create') ? (
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href={employeeCreate.url()}>
|
<a href={employeeCreate.url()}>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Tambah
|
Tambah
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -55,13 +55,12 @@ type CreateColumnsParams = {
|
|||||||
handleDeleteClick: (leaveRequest: LeaveRequest) => void;
|
handleDeleteClick: (leaveRequest: LeaveRequest) => void;
|
||||||
handleApprove: (leaveRequest: LeaveRequest) => void;
|
handleApprove: (leaveRequest: LeaveRequest) => void;
|
||||||
handleReject: (leaveRequest: LeaveRequest) => void;
|
handleReject: (leaveRequest: LeaveRequest) => void;
|
||||||
can: (permission: string) => boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createLeaveRequestColumns(
|
export function createLeaveRequestColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<LeaveRequest>[] {
|
): ColumnDef<LeaveRequest>[] {
|
||||||
const { handleEdit, handleDeleteClick, handleApprove, handleReject, can } =
|
const { handleEdit, handleDeleteClick, handleApprove, handleReject } =
|
||||||
params;
|
params;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -130,9 +129,7 @@ export function createLeaveRequestColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||||
),
|
),
|
||||||
show:
|
show: leaveRequest.status === 'pending',
|
||||||
can('leave_requests.verify') &&
|
|
||||||
leaveRequest.status === 'pending',
|
|
||||||
onClick: () => handleApprove(leaveRequest),
|
onClick: () => handleApprove(leaveRequest),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -140,15 +137,12 @@ export function createLeaveRequestColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<XCircle className="h-4 w-4 text-red-600" />
|
<XCircle className="h-4 w-4 text-red-600" />
|
||||||
),
|
),
|
||||||
show:
|
show: leaveRequest.status === 'pending',
|
||||||
can('leave_requests.verify') &&
|
|
||||||
leaveRequest.status === 'pending',
|
|
||||||
onClick: () => handleReject(leaveRequest),
|
onClick: () => handleReject(leaveRequest),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show: can('leave_requests.update'),
|
|
||||||
onClick: () => handleEdit(leaveRequest),
|
onClick: () => handleEdit(leaveRequest),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -156,7 +150,6 @@ export function createLeaveRequestColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show: can('leave_requests.delete'),
|
|
||||||
onClick: () => handleDeleteClick(leaveRequest),
|
onClick: () => handleDeleteClick(leaveRequest),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import {
|
import {
|
||||||
approve,
|
approve,
|
||||||
@ -53,7 +52,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function LeaveRequestIndex({ leaveRequests, filters, filterOptions }: Props) {
|
export default function LeaveRequestIndex({ leaveRequests, filters, filterOptions }: Props) {
|
||||||
const { can } = useCan();
|
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<LeaveRequest | null>(null);
|
const [editing, setEditing] = useState<LeaveRequest | null>(null);
|
||||||
const [deleting, setDeleting] = useState<LeaveRequest | null>(null);
|
const [deleting, setDeleting] = useState<LeaveRequest | null>(null);
|
||||||
@ -143,7 +141,6 @@ export default function LeaveRequestIndex({ leaveRequests, filters, filterOption
|
|||||||
handleDeleteClick: (leaveRequest) => setDeleting(leaveRequest),
|
handleDeleteClick: (leaveRequest) => setDeleting(leaveRequest),
|
||||||
handleApprove: (leaveRequest) => setApproving(leaveRequest),
|
handleApprove: (leaveRequest) => setApproving(leaveRequest),
|
||||||
handleReject: (leaveRequest) => setRejecting(leaveRequest),
|
handleReject: (leaveRequest) => setRejecting(leaveRequest),
|
||||||
can,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterToolbar = (
|
const filterToolbar = (
|
||||||
@ -184,7 +181,6 @@ export default function LeaveRequestIndex({ leaveRequests, filters, filterOption
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Cuti"
|
title="Cuti"
|
||||||
actions={
|
actions={
|
||||||
can('leave_requests.create') ? (
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -194,7 +190,6 @@ export default function LeaveRequestIndex({ leaveRequests, filters, filterOption
|
|||||||
Tambah
|
Tambah
|
||||||
</button>
|
</button>
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ 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';
|
||||||
import { index as cuttingIndex, store } from '@/routes/admin/manage/cuttings';
|
import { index as cuttingIndex, store } from '@/routes/admin/manage/cuttings';
|
||||||
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
import { Form, Head, usePage } from '@inertiajs/react';
|
||||||
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import type { CuttingCreateData } from './columns';
|
import type { CuttingCreateData } from './columns';
|
||||||
@ -45,7 +45,7 @@ type Props = {
|
|||||||
|
|
||||||
export default function CuttingCreate({ data }: Props) {
|
export default function CuttingCreate({ data }: Props) {
|
||||||
const { rawMaterials } = data;
|
const { rawMaterials } = data;
|
||||||
const { auth, errors } = usePage().props as { auth: { user?: { id?: number } }; errors: Record<string, string> };
|
const { auth } = usePage().props as { auth: { user?: { id?: number } } };
|
||||||
const userId = auth.user?.id;
|
const userId = auth.user?.id;
|
||||||
|
|
||||||
const draft = loadCuttingDraft('create', userId);
|
const draft = loadCuttingDraft('create', userId);
|
||||||
@ -63,7 +63,6 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
photo_url: m.photo_url,
|
photo_url: m.photo_url,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
const [combinations, setCombinations] = useState<CombinationState[]>(() => {
|
const [combinations, setCombinations] = useState<CombinationState[]>(() => {
|
||||||
@ -72,7 +71,6 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
material_result: c.material_result ?? 0,
|
material_result: c.material_result ?? 0,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
const [selectedMaterialName, setSelectedMaterialName] = useState(draft?.selectedMaterialName ?? '');
|
const [selectedMaterialName, setSelectedMaterialName] = useState(draft?.selectedMaterialName ?? '');
|
||||||
@ -151,21 +149,12 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
|
|
||||||
const addVariant = useCallback(
|
const addVariant = useCallback(
|
||||||
(priceId: number) => {
|
(priceId: number) => {
|
||||||
if (!selectedMaterial) {
|
if (!selectedMaterial) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const price = selectedMaterial.raw_material_prices.find((p) => p.id === priceId);
|
const price = selectedMaterial.raw_material_prices.find((p) => p.id === priceId);
|
||||||
|
if (!price) return;
|
||||||
if (!price) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMaterials((prev) => {
|
setMaterials((prev) => {
|
||||||
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) {
|
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) return prev;
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@ -198,26 +187,21 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const confirmCombo = useCallback(() => {
|
const confirmCombo = useCallback(() => {
|
||||||
if (comboSelectedPriceIds.length < 2) {
|
if (comboSelectedPriceIds.length < 2) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const comboIndex = combinations.length;
|
const comboIndex = combinations.length;
|
||||||
|
|
||||||
const newMaterials: MaterialState[] = comboSelectedPriceIds.map((priceId) => {
|
const newMaterials: MaterialState[] = comboSelectedPriceIds.map((priceId) => {
|
||||||
let foundMaterial: typeof rawMaterials[number] | undefined;
|
let foundMaterial: typeof rawMaterials[number] | undefined;
|
||||||
let foundPrice: typeof rawMaterials[number]['raw_material_prices'][number] | undefined;
|
let foundPrice: typeof rawMaterials[number]['raw_material_prices'][number] | undefined;
|
||||||
|
|
||||||
for (const rm of rawMaterials) {
|
for (const rm of rawMaterials) {
|
||||||
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
|
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
|
||||||
|
|
||||||
if (p) {
|
if (p) {
|
||||||
foundMaterial = rm;
|
foundMaterial = rm;
|
||||||
foundPrice = p;
|
foundPrice = p;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
raw_material_price_id: priceId,
|
raw_material_price_id: priceId,
|
||||||
material_usage: 0,
|
material_usage: 0,
|
||||||
@ -254,7 +238,6 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
setMaterials((prev) => {
|
setMaterials((prev) => {
|
||||||
const updated = [...prev];
|
const updated = [...prev];
|
||||||
(updated[index] as Record<string, unknown>)[field] = value;
|
(updated[index] as Record<string, unknown>)[field] = value;
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -268,7 +251,6 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
const totalMaterialCost = useMemo(() => {
|
const totalMaterialCost = useMemo(() => {
|
||||||
return materials.reduce((sum, m) => {
|
return materials.reduce((sum, m) => {
|
||||||
const price = priceMap.get(m.raw_material_price_id);
|
const price = priceMap.get(m.raw_material_price_id);
|
||||||
|
|
||||||
return sum + (price ? price.price * m.material_usage : 0);
|
return sum + (price ? price.price * m.material_usage : 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
}, [materials, priceMap]);
|
}, [materials, priceMap]);
|
||||||
@ -284,9 +266,9 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
return {
|
return {
|
||||||
description: notes || null,
|
description: notes || null,
|
||||||
product_name: productName || null,
|
product_name: productName || null,
|
||||||
sample: sample,
|
sample: sample || null,
|
||||||
original_outside_sample: originalOutsideSample,
|
original_outside_sample: originalOutsideSample || null,
|
||||||
cutting_result: cuttingResult,
|
cutting_result: cuttingResult || null,
|
||||||
materials: materialsRef.current.map((m) => ({
|
materials: materialsRef.current.map((m) => ({
|
||||||
raw_material_price_id: m.raw_material_price_id,
|
raw_material_price_id: m.raw_material_price_id,
|
||||||
material_usage: m.material_usage,
|
material_usage: m.material_usage,
|
||||||
@ -308,16 +290,14 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-2xl font-semibold tracking-tight">Tambah Cutting</h2>
|
<h2 className="text-2xl font-semibold tracking-tight">Tambah Cutting</h2>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={cuttingIndex.url()}>
|
<a href={cuttingIndex.url()}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Kembali
|
Kembali
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form action={store()} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => {
|
<Form action={store()} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => { submittingRef.current = true; }}>
|
||||||
submittingRef.current = true;
|
|
||||||
}}>
|
|
||||||
{({ errors, processing }) => (
|
{({ errors, processing }) => (
|
||||||
<div className="grid gap-6 md:grid-cols-3">
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
<div className="space-y-6 md:col-span-2">
|
<div className="space-y-6 md:col-span-2">
|
||||||
@ -357,7 +337,6 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
{selectedMaterial.raw_material_prices.map((price) => {
|
{selectedMaterial.raw_material_prices.map((price) => {
|
||||||
const isAdded = materials.some((m) => m.raw_material_price_id === price.id);
|
const isAdded = materials.some((m) => m.raw_material_price_id === price.id);
|
||||||
const addedCount = materials.filter((m) => m.raw_material_price_id === price.id).length;
|
const addedCount = materials.filter((m) => m.raw_material_price_id === price.id).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={price.id} className={isAdded ? 'flex items-center justify-between gap-3 rounded-lg border border-primary p-3' : 'flex items-center justify-between gap-3 rounded-lg border p-3'}>
|
<div key={price.id} className={isAdded ? 'flex items-center justify-between gap-3 rounded-lg border border-primary p-3' : 'flex items-center justify-between gap-3 rounded-lg border p-3'}>
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
@ -390,6 +369,8 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@ -440,7 +421,7 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
{cuttingResult > 0 && (
|
{cuttingResult > 0 && (
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Biaya Per Produk</span>
|
<span className="text-muted-foreground">Biaya Per Unit</span>
|
||||||
<span className="font-medium">{formatCurrency(costPerUnit)}</span>
|
<span className="font-medium">{formatCurrency(costPerUnit)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -454,13 +435,11 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>Foto</Label>
|
<Label>Foto</Label>
|
||||||
<FileUpload value={photo} onChange={(key) => {
|
<FileUpload value={photo} onChange={(key) => { setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null); }} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
|
||||||
setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null);
|
|
||||||
}} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
|
|
||||||
<InputError message={errors.photo_key} />
|
<InputError message={errors.photo_key} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="submit" className="w-full" disabled={processing}>
|
<Button type="submit" className="w-full" disabled={processing || submittingRef.current || uploading || materials.length === 0 || !productName || !sample}>
|
||||||
{processing ? 'Menyimpan...' : 'Simpan'}
|
{processing ? 'Menyimpan...' : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -495,10 +474,7 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
|
|
||||||
materials.forEach((m, i) => {
|
materials.forEach((m, i) => {
|
||||||
if (m.combination_id !== null) {
|
if (m.combination_id !== null) {
|
||||||
if (!comboMap.has(m.combination_id)) {
|
if (!comboMap.has(m.combination_id)) comboMap.set(m.combination_id, []);
|
||||||
comboMap.set(m.combination_id, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
comboMap.get(m.combination_id)!.push({ m, index: i });
|
comboMap.get(m.combination_id)!.push({ m, index: i });
|
||||||
} else {
|
} else {
|
||||||
singleItems.push({ m, index: i });
|
singleItems.push({ m, index: i });
|
||||||
@ -511,7 +487,6 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
return groups.map((group, gi) => (
|
return groups.map((group, gi) => (
|
||||||
<div key={gi} className="space-y-2 rounded-lg border p-3">
|
<div key={gi} className="space-y-2 rounded-lg border p-3">
|
||||||
{group.comboIndex !== null && (
|
{group.comboIndex !== null && (
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-between border-b pb-2">
|
<div className="flex items-center justify-between border-b pb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs font-semibold text-muted-foreground">Kombinasi {group.comboIndex + 1}</span>
|
<span className="text-xs font-semibold text-muted-foreground">Kombinasi {group.comboIndex + 1}</span>
|
||||||
@ -530,13 +505,10 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<InputError message={errors[`combinations.${group.comboIndex}.material_result` as keyof typeof errors]} />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{group.items.map(({ m, index }) => {
|
{group.items.map(({ m, index }) => {
|
||||||
const price = priceMap.get(m.raw_material_price_id);
|
const price = priceMap.get(m.raw_material_price_id);
|
||||||
const cartKey = `material-${index}`;
|
const cartKey = `material-${index}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={cartKey} className="space-y-2">
|
<div key={cartKey} className="space-y-2">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
@ -570,7 +542,6 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
onValueChange={(val) => updateMaterial(index, 'material_usage', val)}
|
onValueChange={(val) => updateMaterial(index, 'material_usage', val)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<InputError message={errors[`materials.${index}.material_usage` as keyof typeof errors]} />
|
|
||||||
{group.comboIndex === null && (
|
{group.comboIndex === null && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-muted-foreground whitespace-nowrap">Hasil <span className="text-destructive">*</span></span>
|
<span className="text-xs text-muted-foreground whitespace-nowrap">Hasil <span className="text-destructive">*</span></span>
|
||||||
@ -581,7 +552,6 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{group.comboIndex === null && <InputError message={errors[`materials.${index}.material_result` as keyof typeof errors]} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -601,11 +571,7 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
|
|
||||||
<ImagePreviewModal
|
<ImagePreviewModal
|
||||||
open={previewKey !== null}
|
open={previewKey !== null}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => { if (!open) setPreviewKey(null); }}
|
||||||
if (!open) {
|
|
||||||
setPreviewKey(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
src={previewKey && previewKey.startsWith('material-') ? (materials[Number(previewKey.replace('material-', ''))]?.photo_url ?? null) : null}
|
src={previewKey && previewKey.startsWith('material-') ? (materials[Number(previewKey.replace('material-', ''))]?.photo_url ?? null) : null}
|
||||||
title={previewKey && previewKey.startsWith('material-') ? `${materials[Number(previewKey.replace('material-', ''))]?.material_name} — ${materials[Number(previewKey.replace('material-', ''))]?.variant}` : undefined}
|
title={previewKey && previewKey.startsWith('material-') ? `${materials[Number(previewKey.replace('material-', ''))]?.material_name} — ${materials[Number(previewKey.replace('material-', ''))]?.variant}` : undefined}
|
||||||
sources={materials.filter((m) => m.photo_url).map((m) => m.photo_url!)}
|
sources={materials.filter((m) => m.photo_url).map((m) => m.photo_url!)}
|
||||||
@ -653,10 +619,8 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
let variantName = '';
|
let variantName = '';
|
||||||
let materialName = '';
|
let materialName = '';
|
||||||
let photoUrl: string | null = null;
|
let photoUrl: string | null = null;
|
||||||
|
|
||||||
for (const rm of rawMaterials) {
|
for (const rm of rawMaterials) {
|
||||||
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
|
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
|
||||||
|
|
||||||
if (p) {
|
if (p) {
|
||||||
variantName = p.variant;
|
variantName = p.variant;
|
||||||
materialName = rm.name;
|
materialName = rm.name;
|
||||||
@ -664,7 +628,6 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={priceId} className="flex items-center justify-between gap-2 rounded-md border border-primary bg-primary/5 px-3 py-2">
|
<div key={priceId} className="flex items-center justify-between gap-2 rounded-md border border-primary bg-primary/5 px-3 py-2">
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
@ -694,7 +657,6 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{comboMaterial.raw_material_prices.map((price) => {
|
{comboMaterial.raw_material_prices.map((price) => {
|
||||||
const isSelected = comboSelectedPriceIds.includes(price.id);
|
const isSelected = comboSelectedPriceIds.includes(price.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={price.id} className={`flex items-center justify-between gap-3 rounded-lg border p-3 ${isSelected ? 'border-primary' : ''}`}>
|
<div key={price.id} className={`flex items-center justify-between gap-3 rounded-lg border p-3 ${isSelected ? 'border-primary' : ''}`}>
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
@ -727,41 +689,11 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => {
|
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => { if (!open) { setDeleteConfirmOpen(false); setDeleteMaterialIndex(null); } }} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => { if (deleteMaterialIndex !== null) setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex)); setDeleteConfirmOpen(false); setDeleteMaterialIndex(null); }} />
|
||||||
if (!open) {
|
|
||||||
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
|
|
||||||
}
|
|
||||||
}} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => {
|
|
||||||
if (deleteMaterialIndex !== null) {
|
|
||||||
setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
|
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => { if (!open) { setCartDeleteConfirmOpen(false); setCartDeleteIndex(null); } }} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => { if (cartDeleteIndex !== null) setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex)); setCartDeleteConfirmOpen(false); setCartDeleteIndex(null); }} />
|
||||||
}} />
|
|
||||||
|
|
||||||
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => {
|
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => { if (!open) { setComboDeleteConfirmOpen(false); setComboDeleteIndex(null); } }} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => { if (comboDeleteIndex !== null) { setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex)); } setComboDeleteConfirmOpen(false); setComboDeleteIndex(null); }} />
|
||||||
if (!open) {
|
|
||||||
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
|
|
||||||
}
|
|
||||||
}} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
|
|
||||||
if (cartDeleteIndex !== null) {
|
|
||||||
setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
|
|
||||||
}} />
|
|
||||||
|
|
||||||
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
|
|
||||||
}
|
|
||||||
}} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
|
|
||||||
if (comboDeleteIndex !== null) {
|
|
||||||
setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
|
|
||||||
}} />
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
|
import { ChevronDown, Pencil, Trash2 } from 'lucide-react';
|
||||||
import { ImagePreviewButton } from '@/components/image-preview-button';
|
import { ImagePreviewButton } from '@/components/image-preview-button';
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { formatDateTime, formatNumber } from '@/lib/format';
|
import { formatDateTime, formatNumber } from '@/lib/format';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import { ChevronDown, Pencil, Trash2 } from 'lucide-react';
|
|
||||||
import type { Cutting } from './columns';
|
import type { Cutting } from './columns';
|
||||||
|
|
||||||
export type CuttingCardRowParams = {
|
export type CuttingCardRowParams = {
|
||||||
@ -25,18 +24,9 @@ export function CuttingCardRow({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: CuttingCardRowParams) {
|
}: CuttingCardRowParams) {
|
||||||
const { can } = useCan();
|
|
||||||
const items = cutting.cutting_materials ?? [];
|
const items = cutting.cutting_materials ?? [];
|
||||||
const result = cutting.cutting_results?.[0];
|
const result = cutting.cutting_results?.[0];
|
||||||
const singleCount = items.filter(
|
const materialCount = items.length;
|
||||||
(item) => item.combination_id === null,
|
|
||||||
).length;
|
|
||||||
const comboCount = new Set(
|
|
||||||
items
|
|
||||||
.filter((item) => item.combination_id !== null)
|
|
||||||
.map((item) => item.combination_id),
|
|
||||||
).size;
|
|
||||||
const materialCount = singleCount + comboCount;
|
|
||||||
const productName = result?.product_name ?? '-';
|
const productName = result?.product_name ?? '-';
|
||||||
const totalUsage = items.reduce(
|
const totalUsage = items.reduce(
|
||||||
(sum, item) => sum + Number(item.material_usage),
|
(sum, item) => sum + Number(item.material_usage),
|
||||||
@ -113,19 +103,11 @@ export function CuttingCardRow({
|
|||||||
{cutting.cost_per_unit && (
|
{cutting.cost_per_unit && (
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
<span className="font-normal text-muted-foreground">
|
<span className="font-normal text-muted-foreground">
|
||||||
Per Produk:{' '}
|
Per Unit:{' '}
|
||||||
</span>
|
</span>
|
||||||
{formatCurrency(cutting.cost_per_unit)}
|
{formatCurrency(cutting.cost_per_unit)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{cutting.total_material_cost && (
|
|
||||||
<span className="font-semibold">
|
|
||||||
<span className="font-normal text-muted-foreground">
|
|
||||||
Biaya Keseluruhan:{' '}
|
|
||||||
</span>
|
|
||||||
{formatCurrency(cutting.total_material_cost)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{cutting.photo_url && (
|
{cutting.photo_url && (
|
||||||
@ -144,7 +126,6 @@ export function CuttingCardRow({
|
|||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show: can('cuttings.update'),
|
|
||||||
onClick: () => onEdit(cutting),
|
onClick: () => onEdit(cutting),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -152,7 +133,6 @@ export function CuttingCardRow({
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show: can('cuttings.delete'),
|
|
||||||
onClick: () => onDelete(cutting),
|
onClick: () => onDelete(cutting),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -8,41 +8,55 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { formatNumber } from '@/lib/format';
|
import { formatNumber } from '@/lib/format';
|
||||||
import { Fragment } from 'react';
|
|
||||||
import type { Cutting } from './columns';
|
import type { Cutting } from './columns';
|
||||||
|
|
||||||
export function CuttingItemSubRow({ cutting }: { cutting: Cutting }) {
|
export function CuttingItemSubRow({ cutting }: { cutting: Cutting }) {
|
||||||
const items = cutting.cutting_materials ?? [];
|
const items = cutting.cutting_materials ?? [];
|
||||||
const combinations = cutting.cutting_material_combinations ?? [];
|
const combinations = cutting.cutting_material_combinations ?? [];
|
||||||
|
|
||||||
const singles = items.filter((item) => item.combination_id === null);
|
const groupedMaterials = items.reduce(
|
||||||
const comboItems = items.filter((item) => item.combination_id !== null);
|
|
||||||
|
|
||||||
const comboGroups: Record<number, typeof items> = {};
|
|
||||||
comboItems.forEach((item) => {
|
|
||||||
const comboId = item.combination_id!;
|
|
||||||
if (!comboGroups[comboId]) comboGroups[comboId] = [];
|
|
||||||
comboGroups[comboId].push(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
const singleByRawMaterial = singles.reduce(
|
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
const name =
|
const key = item.combination_id ?? `single-${item.id}`;
|
||||||
item.raw_material_price?.raw_material?.name ?? 'BING';
|
|
||||||
if (!acc[name]) acc[name] = [];
|
if (!acc[key]) {
|
||||||
acc[name].push(item);
|
acc[key] = {
|
||||||
|
combination: item.combination_id
|
||||||
|
? (combinations.find((c) => c.id === item.combination_id) ?? null)
|
||||||
|
: null,
|
||||||
|
materials: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[key].materials.push(item);
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<string, typeof singles>,
|
{} as Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
combination: { id: number; material_result: number | null } | null;
|
||||||
|
materials: typeof items;
|
||||||
|
}
|
||||||
|
>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasSingle = singles.length > 0;
|
|
||||||
const hasCombo = comboItems.length > 0;
|
|
||||||
|
|
||||||
let counter = 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="space-y-4 overflow-x-auto">
|
||||||
|
{Object.entries(groupedMaterials).map(([key, group]) => (
|
||||||
|
<div key={key} className="space-y-2">
|
||||||
|
{group.combination && (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 font-medium text-foreground">
|
||||||
|
Kombinasi
|
||||||
|
</span>
|
||||||
|
{group.combination.material_result !== null && (
|
||||||
|
<span>
|
||||||
|
Hasil: {formatNumber(group.combination.material_result)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@ -50,222 +64,68 @@ export function CuttingItemSubRow({ cutting }: { cutting: Cutting }) {
|
|||||||
No
|
No
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[60px]">Foto</TableHead>
|
<TableHead className="w-[60px]">Foto</TableHead>
|
||||||
|
<TableHead>Bahan Baku</TableHead>
|
||||||
<TableHead>Varian</TableHead>
|
<TableHead>Varian</TableHead>
|
||||||
<TableHead className="text-right">
|
<TableHead className="text-right">Pemakaian</TableHead>
|
||||||
Pemakaian
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="text-right">Hasil</TableHead>
|
<TableHead className="text-right">Hasil</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{hasSingle && (
|
{group.materials.length === 0 ? (
|
||||||
<>
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={5}
|
colSpan={6}
|
||||||
className="text-center font-bold bg-muted/50"
|
|
||||||
>
|
|
||||||
Single
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
{Object.entries(singleByRawMaterial).map(
|
|
||||||
([rawMaterialName, groupItems]) => {
|
|
||||||
const totalPemakaian = groupItems.reduce(
|
|
||||||
(sum, item) =>
|
|
||||||
sum + Number(item.material_usage),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const totalHasil = groupItems.reduce(
|
|
||||||
(sum, item) =>
|
|
||||||
sum +
|
|
||||||
(item.material_result !== null
|
|
||||||
? Number(item.material_result)
|
|
||||||
: 0),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment key={rawMaterialName}>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={3}
|
|
||||||
className="text-center font-medium text-muted-foreground bg-muted/20"
|
|
||||||
>
|
|
||||||
{rawMaterialName}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right font-medium text-muted-foreground bg-muted/20">
|
|
||||||
{formatNumber(
|
|
||||||
totalPemakaian,
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right font-medium text-muted-foreground bg-muted/20">
|
|
||||||
{formatNumber(
|
|
||||||
totalHasil,
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
{groupItems.map((item) => {
|
|
||||||
counter++;
|
|
||||||
return (
|
|
||||||
<TableRow key={item.id}>
|
|
||||||
<TableCell className="text-center">
|
|
||||||
{counter}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{item.raw_material_price
|
|
||||||
?.photo_url ? (
|
|
||||||
<ImagePreviewButton
|
|
||||||
srcs={[
|
|
||||||
item
|
|
||||||
.raw_material_price
|
|
||||||
.photo_url,
|
|
||||||
]}
|
|
||||||
title={
|
|
||||||
item
|
|
||||||
.raw_material_price
|
|
||||||
.variant
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">
|
|
||||||
N/A
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{item.raw_material_price
|
|
||||||
?.variant ?? '-'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
{formatNumber(
|
|
||||||
item.material_usage,
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
{item.material_result !==
|
|
||||||
null
|
|
||||||
? formatNumber(
|
|
||||||
item.material_result,
|
|
||||||
)
|
|
||||||
: '-'}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasCombo && (
|
|
||||||
<>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={5}
|
|
||||||
className="text-center font-bold bg-muted/50"
|
|
||||||
>
|
|
||||||
Kombinasi
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
{Object.entries(comboGroups).map(
|
|
||||||
([comboId, materials]) => {
|
|
||||||
counter++;
|
|
||||||
const combo = combinations.find(
|
|
||||||
(c) => c.id === Number(comboId),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment key={comboId}>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell className="text-center font-medium">
|
|
||||||
{counter}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
colSpan={4}
|
|
||||||
className="font-medium text-muted-foreground bg-muted/20"
|
|
||||||
>
|
|
||||||
Kombinasi{' '}
|
|
||||||
{counter}
|
|
||||||
{combo?.material_result !==
|
|
||||||
null &&
|
|
||||||
combo?.material_result !==
|
|
||||||
undefined && (
|
|
||||||
<span className="ml-2">
|
|
||||||
(Hasil:{' '}
|
|
||||||
{formatNumber(
|
|
||||||
combo.material_result,
|
|
||||||
)}
|
|
||||||
)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
{materials.map((item) => (
|
|
||||||
<TableRow key={item.id}>
|
|
||||||
<TableCell></TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{item
|
|
||||||
.raw_material_price
|
|
||||||
?.photo_url ? (
|
|
||||||
<ImagePreviewButton
|
|
||||||
srcs={[
|
|
||||||
item
|
|
||||||
.raw_material_price
|
|
||||||
.photo_url,
|
|
||||||
]}
|
|
||||||
title={
|
|
||||||
item
|
|
||||||
.raw_material_price
|
|
||||||
.variant
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">
|
|
||||||
N/A
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{item
|
|
||||||
.raw_material_price
|
|
||||||
?.raw_material
|
|
||||||
?.name ?? '-'}{' '}
|
|
||||||
-{' '}
|
|
||||||
{item
|
|
||||||
.raw_material_price
|
|
||||||
?.variant ?? '-'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
{formatNumber(
|
|
||||||
item.material_usage,
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
-
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{items.length === 0 && (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={5}
|
|
||||||
className="text-center text-muted-foreground"
|
className="text-center text-muted-foreground"
|
||||||
>
|
>
|
||||||
Tidak ada bahan baku.
|
Tidak ada item.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
group.materials.map((item, index) => (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{index + 1}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.raw_material_price?.photo_url ? (
|
||||||
|
<ImagePreviewButton
|
||||||
|
srcs={[
|
||||||
|
item.raw_material_price
|
||||||
|
.photo_url,
|
||||||
|
]}
|
||||||
|
title={
|
||||||
|
item.raw_material_price.variant
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">
|
||||||
|
N/A
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.raw_material_price?.raw_material?.name ?? '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.raw_material_price?.variant ?? '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{formatNumber(item.material_usage)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{group.combination
|
||||||
|
? '-'
|
||||||
|
: (item.material_result !== null
|
||||||
|
? formatNumber(item.material_result)
|
||||||
|
: '-')}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ 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';
|
||||||
import { index as cuttingIndex, update } from '@/routes/admin/manage/cuttings';
|
import { index as cuttingIndex, update } from '@/routes/admin/manage/cuttings';
|
||||||
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
import { Form, Head } from '@inertiajs/react';
|
||||||
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import type { CuttingCreateData, CuttingForEdit } from './columns';
|
import type { CuttingCreateData, CuttingForEdit } from './columns';
|
||||||
@ -46,7 +46,6 @@ type Props = {
|
|||||||
|
|
||||||
export default function CuttingEdit({ cutting, data }: Props) {
|
export default function CuttingEdit({ cutting, data }: Props) {
|
||||||
const { rawMaterials } = data;
|
const { rawMaterials } = data;
|
||||||
const { errors } = usePage().props as { errors: Record<string, string> };
|
|
||||||
|
|
||||||
const [materials, setMaterials] = useState<MaterialState[]>(() => {
|
const [materials, setMaterials] = useState<MaterialState[]>(() => {
|
||||||
const comboIdToIndex = new Map<number, number>();
|
const comboIdToIndex = new Map<number, number>();
|
||||||
@ -132,21 +131,12 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
|
|
||||||
const addVariant = useCallback(
|
const addVariant = useCallback(
|
||||||
(priceId: number) => {
|
(priceId: number) => {
|
||||||
if (!selectedMaterial) {
|
if (!selectedMaterial) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const price = selectedMaterial.raw_material_prices.find((p) => p.id === priceId);
|
const price = selectedMaterial.raw_material_prices.find((p) => p.id === priceId);
|
||||||
|
if (!price) return;
|
||||||
if (!price) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMaterials((prev) => {
|
setMaterials((prev) => {
|
||||||
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) {
|
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) return prev;
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@ -179,26 +169,21 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const confirmCombo = useCallback(() => {
|
const confirmCombo = useCallback(() => {
|
||||||
if (comboSelectedPriceIds.length < 2) {
|
if (comboSelectedPriceIds.length < 2) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const comboIndex = combinations.length;
|
const comboIndex = combinations.length;
|
||||||
|
|
||||||
const newMaterials: MaterialState[] = comboSelectedPriceIds.map((priceId) => {
|
const newMaterials: MaterialState[] = comboSelectedPriceIds.map((priceId) => {
|
||||||
let foundMaterial: (typeof rawMaterials)[number] | undefined;
|
let foundMaterial: (typeof rawMaterials)[number] | undefined;
|
||||||
let foundPrice: (typeof rawMaterials)[number]['raw_material_prices'][number] | undefined;
|
let foundPrice: (typeof rawMaterials)[number]['raw_material_prices'][number] | undefined;
|
||||||
|
|
||||||
for (const rm of rawMaterials) {
|
for (const rm of rawMaterials) {
|
||||||
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
|
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
|
||||||
|
|
||||||
if (p) {
|
if (p) {
|
||||||
foundMaterial = rm;
|
foundMaterial = rm;
|
||||||
foundPrice = p;
|
foundPrice = p;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
raw_material_price_id: priceId,
|
raw_material_price_id: priceId,
|
||||||
material_usage: 0,
|
material_usage: 0,
|
||||||
@ -230,7 +215,6 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
setMaterials((prev) => {
|
setMaterials((prev) => {
|
||||||
const updated = [...prev];
|
const updated = [...prev];
|
||||||
(updated[index] as Record<string, unknown>)[field] = value;
|
(updated[index] as Record<string, unknown>)[field] = value;
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -244,7 +228,6 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
const totalMaterialCost = useMemo(() => {
|
const totalMaterialCost = useMemo(() => {
|
||||||
return materials.reduce((sum, m) => {
|
return materials.reduce((sum, m) => {
|
||||||
const price = priceMap.get(m.raw_material_price_id);
|
const price = priceMap.get(m.raw_material_price_id);
|
||||||
|
|
||||||
return sum + (price ? price.price * m.material_usage : 0);
|
return sum + (price ? price.price * m.material_usage : 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
}, [materials, priceMap]);
|
}, [materials, priceMap]);
|
||||||
@ -260,9 +243,9 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
return {
|
return {
|
||||||
description: notes || null,
|
description: notes || null,
|
||||||
product_name: productName || null,
|
product_name: productName || null,
|
||||||
sample: sample,
|
sample: sample || null,
|
||||||
original_outside_sample: originalOutsideSample,
|
original_outside_sample: originalOutsideSample || null,
|
||||||
cutting_result: cuttingResult,
|
cutting_result: cuttingResult || null,
|
||||||
materials: materialsRef.current.map((m) => ({
|
materials: materialsRef.current.map((m) => ({
|
||||||
raw_material_price_id: m.raw_material_price_id,
|
raw_material_price_id: m.raw_material_price_id,
|
||||||
material_usage: m.material_usage,
|
material_usage: m.material_usage,
|
||||||
@ -284,16 +267,14 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-2xl font-semibold tracking-tight">Edit Cutting</h2>
|
<h2 className="text-2xl font-semibold tracking-tight">Edit Cutting</h2>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={cuttingIndex.url()}>
|
<a href={cuttingIndex.url()}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Kembali
|
Kembali
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form action={update(cutting.id)} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => {
|
<Form action={update(cutting.id)} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => { submittingRef.current = true; }}>
|
||||||
submittingRef.current = true;
|
|
||||||
}}>
|
|
||||||
{({ errors, processing }) => (
|
{({ errors, processing }) => (
|
||||||
<div className="grid gap-6 md:grid-cols-3">
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
<div className="space-y-6 md:col-span-2">
|
<div className="space-y-6 md:col-span-2">
|
||||||
@ -333,7 +314,6 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
{selectedMaterial.raw_material_prices.map((price) => {
|
{selectedMaterial.raw_material_prices.map((price) => {
|
||||||
const isAdded = materials.some((m) => m.raw_material_price_id === price.id);
|
const isAdded = materials.some((m) => m.raw_material_price_id === price.id);
|
||||||
const addedCount = materials.filter((m) => m.raw_material_price_id === price.id).length;
|
const addedCount = materials.filter((m) => m.raw_material_price_id === price.id).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={price.id} className={isAdded ? 'flex items-center justify-between gap-3 rounded-lg border border-primary p-3' : 'flex items-center justify-between gap-3 rounded-lg border p-3'}>
|
<div key={price.id} className={isAdded ? 'flex items-center justify-between gap-3 rounded-lg border border-primary p-3' : 'flex items-center justify-between gap-3 rounded-lg border p-3'}>
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
@ -416,7 +396,7 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
{cuttingResult > 0 && (
|
{cuttingResult > 0 && (
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Biaya Per Produk</span>
|
<span className="text-muted-foreground">Biaya Per Unit</span>
|
||||||
<span className="font-medium">{formatCurrency(costPerUnit)}</span>
|
<span className="font-medium">{formatCurrency(costPerUnit)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -430,13 +410,11 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>Foto</Label>
|
<Label>Foto</Label>
|
||||||
<FileUpload value={photo} onChange={(key) => {
|
<FileUpload value={photo} onChange={(key) => { setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null); }} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
|
||||||
setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null);
|
|
||||||
}} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
|
|
||||||
<InputError message={errors.photo_key} />
|
<InputError message={errors.photo_key} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="submit" className="w-full" disabled={processing}>
|
<Button type="submit" className="w-full" disabled={processing || submittingRef.current || uploading || materials.length === 0 || !productName || !sample}>
|
||||||
{processing ? 'Menyimpan...' : 'Simpan'}
|
{processing ? 'Menyimpan...' : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -471,10 +449,7 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
|
|
||||||
materials.forEach((m, i) => {
|
materials.forEach((m, i) => {
|
||||||
if (m.combination_id !== null) {
|
if (m.combination_id !== null) {
|
||||||
if (!comboMap.has(m.combination_id)) {
|
if (!comboMap.has(m.combination_id)) comboMap.set(m.combination_id, []);
|
||||||
comboMap.set(m.combination_id, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
comboMap.get(m.combination_id)!.push({ m, index: i });
|
comboMap.get(m.combination_id)!.push({ m, index: i });
|
||||||
} else {
|
} else {
|
||||||
singleItems.push({ m, index: i });
|
singleItems.push({ m, index: i });
|
||||||
@ -487,7 +462,6 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
return groups.map((group, gi) => (
|
return groups.map((group, gi) => (
|
||||||
<div key={gi} className="space-y-2 rounded-lg border p-3">
|
<div key={gi} className="space-y-2 rounded-lg border p-3">
|
||||||
{group.comboIndex !== null && (
|
{group.comboIndex !== null && (
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-between border-b pb-2">
|
<div className="flex items-center justify-between border-b pb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs font-semibold text-muted-foreground">Kombinasi {group.comboIndex + 1}</span>
|
<span className="text-xs font-semibold text-muted-foreground">Kombinasi {group.comboIndex + 1}</span>
|
||||||
@ -506,13 +480,10 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<InputError message={errors[`combinations.${group.comboIndex}.material_result` as keyof typeof errors]} />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{group.items.map(({ m, index }) => {
|
{group.items.map(({ m, index }) => {
|
||||||
const price = priceMap.get(m.raw_material_price_id);
|
const price = priceMap.get(m.raw_material_price_id);
|
||||||
const cartKey = `material-${index}`;
|
const cartKey = `material-${index}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={cartKey} className="space-y-2">
|
<div key={cartKey} className="space-y-2">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
@ -546,7 +517,6 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
onValueChange={(val) => updateMaterial(index, 'material_usage', val)}
|
onValueChange={(val) => updateMaterial(index, 'material_usage', val)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<InputError message={errors[`materials.${index}.material_usage` as keyof typeof errors]} />
|
|
||||||
{group.comboIndex === null && (
|
{group.comboIndex === null && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-muted-foreground whitespace-nowrap">Hasil <span className="text-destructive">*</span></span>
|
<span className="text-xs text-muted-foreground whitespace-nowrap">Hasil <span className="text-destructive">*</span></span>
|
||||||
@ -557,7 +527,6 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{group.comboIndex === null && <InputError message={errors[`materials.${index}.material_result` as keyof typeof errors]} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -577,11 +546,7 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
|
|
||||||
<ImagePreviewModal
|
<ImagePreviewModal
|
||||||
open={previewKey !== null}
|
open={previewKey !== null}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => { if (!open) setPreviewKey(null); }}
|
||||||
if (!open) {
|
|
||||||
setPreviewKey(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
src={previewKey && previewKey.startsWith('material-') ? (materials[Number(previewKey.replace('material-', ''))]?.photo_url ?? null) : null}
|
src={previewKey && previewKey.startsWith('material-') ? (materials[Number(previewKey.replace('material-', ''))]?.photo_url ?? null) : null}
|
||||||
title={previewKey && previewKey.startsWith('material-') ? `${materials[Number(previewKey.replace('material-', ''))]?.material_name} — ${materials[Number(previewKey.replace('material-', ''))]?.variant}` : undefined}
|
title={previewKey && previewKey.startsWith('material-') ? `${materials[Number(previewKey.replace('material-', ''))]?.material_name} — ${materials[Number(previewKey.replace('material-', ''))]?.variant}` : undefined}
|
||||||
sources={materials.filter((m) => m.photo_url).map((m) => m.photo_url!)}
|
sources={materials.filter((m) => m.photo_url).map((m) => m.photo_url!)}
|
||||||
@ -629,10 +594,8 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
let variantName = '';
|
let variantName = '';
|
||||||
let materialName = '';
|
let materialName = '';
|
||||||
let photoUrl: string | null = null;
|
let photoUrl: string | null = null;
|
||||||
|
|
||||||
for (const rm of rawMaterials) {
|
for (const rm of rawMaterials) {
|
||||||
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
|
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
|
||||||
|
|
||||||
if (p) {
|
if (p) {
|
||||||
variantName = p.variant;
|
variantName = p.variant;
|
||||||
materialName = rm.name;
|
materialName = rm.name;
|
||||||
@ -640,7 +603,6 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={priceId} className="flex items-center justify-between gap-2 rounded-md border border-primary bg-primary/5 px-3 py-2">
|
<div key={priceId} className="flex items-center justify-between gap-2 rounded-md border border-primary bg-primary/5 px-3 py-2">
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
@ -670,7 +632,6 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{comboMaterial.raw_material_prices.map((price) => {
|
{comboMaterial.raw_material_prices.map((price) => {
|
||||||
const isSelected = comboSelectedPriceIds.includes(price.id);
|
const isSelected = comboSelectedPriceIds.includes(price.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={price.id} className={`flex items-center justify-between gap-3 rounded-lg border p-3 ${isSelected ? 'border-primary' : ''}`}>
|
<div key={price.id} className={`flex items-center justify-between gap-3 rounded-lg border p-3 ${isSelected ? 'border-primary' : ''}`}>
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
@ -703,41 +664,11 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => {
|
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => { if (!open) { setDeleteConfirmOpen(false); setDeleteMaterialIndex(null); } }} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => { if (deleteMaterialIndex !== null) setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex)); setDeleteConfirmOpen(false); setDeleteMaterialIndex(null); }} />
|
||||||
if (!open) {
|
|
||||||
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
|
|
||||||
}
|
|
||||||
}} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => {
|
|
||||||
if (deleteMaterialIndex !== null) {
|
|
||||||
setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
|
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => { if (!open) { setCartDeleteConfirmOpen(false); setCartDeleteIndex(null); } }} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => { if (cartDeleteIndex !== null) setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex)); setCartDeleteConfirmOpen(false); setCartDeleteIndex(null); }} />
|
||||||
}} />
|
|
||||||
|
|
||||||
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => {
|
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => { if (!open) { setComboDeleteConfirmOpen(false); setComboDeleteIndex(null); } }} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => { if (comboDeleteIndex !== null) { setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex)); } setComboDeleteConfirmOpen(false); setComboDeleteIndex(null); }} />
|
||||||
if (!open) {
|
|
||||||
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
|
|
||||||
}
|
|
||||||
}} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
|
|
||||||
if (cartDeleteIndex !== null) {
|
|
||||||
setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
|
|
||||||
}} />
|
|
||||||
|
|
||||||
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
|
|
||||||
}
|
|
||||||
}} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
|
|
||||||
if (comboDeleteIndex !== null) {
|
|
||||||
setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
|
|
||||||
}} />
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Head, Link, router } from '@inertiajs/react';
|
import { Head, router } from '@inertiajs/react';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { CardTable } from '@/components/card-table';
|
import { CardTable } from '@/components/card-table';
|
||||||
@ -6,7 +6,6 @@ import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
|||||||
import { useCardTableExpand } from '@/components/hooks/use-card-table-expand';
|
import { useCardTableExpand } from '@/components/hooks/use-card-table-expand';
|
||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import {
|
import {
|
||||||
destroy,
|
destroy,
|
||||||
@ -29,7 +28,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function CuttingIndex({ cuttings }: Props) {
|
export default function CuttingIndex({ cuttings }: Props) {
|
||||||
const { can } = useCan();
|
|
||||||
const [deleting, setDeleting] = useState<Cutting | null>(null);
|
const [deleting, setDeleting] = useState<Cutting | null>(null);
|
||||||
const expand = useCardTableExpand(true);
|
const expand = useCardTableExpand(true);
|
||||||
|
|
||||||
@ -68,14 +66,12 @@ export default function CuttingIndex({ cuttings }: Props) {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Cutting"
|
title="Cutting"
|
||||||
actions={
|
actions={
|
||||||
can('cuttings.create') ? (
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href={cuttingCreate.url()}>
|
<a href={cuttingCreate.url()}>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Tambah
|
Tambah
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -90,11 +86,6 @@ export default function CuttingIndex({ cuttings }: Props) {
|
|||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onPerPageChange={handlePerPageChange}
|
onPerPageChange={handlePerPageChange}
|
||||||
rowClassName={(c) =>
|
|
||||||
c.status === 'in_progress'
|
|
||||||
? 'border-l-4 border-l-yellow-500'
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
renderCard={({
|
renderCard={({
|
||||||
item,
|
item,
|
||||||
index,
|
index,
|
||||||
@ -112,7 +103,7 @@ export default function CuttingIndex({ cuttings }: Props) {
|
|||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
onToggleExpand={onToggleExpand}
|
onToggleExpand={onToggleExpand}
|
||||||
onEdit={(c) => {
|
onEdit={(c) => {
|
||||||
router.visit(cuttingEdit.url(c.id));
|
window.location.href = cuttingEdit.url(c.id);
|
||||||
}}
|
}}
|
||||||
onDelete={(c) => setDeleting(c)}
|
onDelete={(c) => setDeleting(c)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use no memo';
|
'use no memo';
|
||||||
|
|
||||||
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
import { Form, Head, usePage } from '@inertiajs/react';
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Check,
|
Check,
|
||||||
@ -360,7 +360,7 @@ export default function PurchaseCreate({ data }: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.sort((a, b) => a.title.localeCompare(b.title));
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
return variants
|
return variants
|
||||||
@ -428,10 +428,10 @@ export default function PurchaseCreate({ data }: Props) {
|
|||||||
Tambah Belanja
|
Tambah Belanja
|
||||||
</h2>
|
</h2>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={purchaseIndex.url()}>
|
<a href={purchaseIndex.url()}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Kembali
|
Kembali
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
'use no memo';
|
'use no memo';
|
||||||
|
|
||||||
import { Form, Head, Link } from '@inertiajs/react';
|
import { Form, Head } from '@inertiajs/react';
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
Check,
|
||||||
|
ClipboardPaste,
|
||||||
|
Copy,
|
||||||
Minus,
|
Minus,
|
||||||
Plus,
|
Plus,
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
import { FileUpload } from '@/components/file-upload';
|
import { FileUpload } from '@/components/file-upload';
|
||||||
import { ImagePreviewModal } from '@/components/image-preview-modal';
|
import { ImagePreviewModal } from '@/components/image-preview-modal';
|
||||||
@ -25,6 +28,7 @@ import {
|
|||||||
ComboboxItem,
|
ComboboxItem,
|
||||||
ComboboxList,
|
ComboboxList,
|
||||||
} from '@/components/ui/combobox';
|
} from '@/components/ui/combobox';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
@ -33,8 +37,10 @@ import {
|
|||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
} from '@/components/ui/sheet';
|
} from '@/components/ui/sheet';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { formatNumber } from '@/lib/format';
|
import { formatNumber } from '@/lib/format';
|
||||||
|
import { getTemporaryUrl } from '@/lib/upload';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
index as purchaseIndex,
|
index as purchaseIndex,
|
||||||
@ -42,6 +48,16 @@ import {
|
|||||||
} from '@/routes/admin/manage/purchases';
|
} from '@/routes/admin/manage/purchases';
|
||||||
import type { PurchaseCreateData, PurchaseForEdit } from './columns';
|
import type { PurchaseCreateData, PurchaseForEdit } from './columns';
|
||||||
|
|
||||||
|
type VariantState = {
|
||||||
|
id?: number;
|
||||||
|
variant: string;
|
||||||
|
price: number;
|
||||||
|
stock: number;
|
||||||
|
photo: string | null;
|
||||||
|
photoUrl: string | null;
|
||||||
|
uploading: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type CartLine = {
|
type CartLine = {
|
||||||
key: string;
|
key: string;
|
||||||
photoUrl: string | null;
|
photoUrl: string | null;
|
||||||
@ -62,6 +78,19 @@ type Props = {
|
|||||||
export default function PurchaseEdit({ purchase, data }: Props) {
|
export default function PurchaseEdit({ purchase, data }: Props) {
|
||||||
const { suppliers, rawMaterials } = data;
|
const { suppliers, rawMaterials } = data;
|
||||||
|
|
||||||
|
const [name] = useState(purchase.name);
|
||||||
|
const [variants, setVariants] = useState<VariantState[]>(() =>
|
||||||
|
purchase.variants.map((v) => ({
|
||||||
|
id: v.id,
|
||||||
|
variant: v.variant,
|
||||||
|
price: v.price,
|
||||||
|
stock: v.stock,
|
||||||
|
photo: v.photo_key ?? null,
|
||||||
|
photoUrl: v.photo_url ?? null,
|
||||||
|
uploading: false,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
const [supplierId, setSupplierId] = useState(String(purchase.supplier_id));
|
const [supplierId, setSupplierId] = useState(String(purchase.supplier_id));
|
||||||
const selectedSupplier =
|
const selectedSupplier =
|
||||||
suppliers.find((s) => String(s.id) === supplierId) ?? null;
|
suppliers.find((s) => String(s.id) === supplierId) ?? null;
|
||||||
@ -72,6 +101,7 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
|||||||
const [photoUrl, setPhotoUrl] = useState<string | null>(purchase.photo_url);
|
const [photoUrl, setPhotoUrl] = useState<string | null>(purchase.photo_url);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
|
const [mode, setMode] = useState<'new' | 'existing'>(purchase.default_mode);
|
||||||
const [selectedMaterialName, setSelectedMaterialName] = useState(
|
const [selectedMaterialName, setSelectedMaterialName] = useState(
|
||||||
purchase.existing_material_name ?? '',
|
purchase.existing_material_name ?? '',
|
||||||
);
|
);
|
||||||
@ -87,6 +117,9 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
|||||||
const [previewKey, setPreviewKey] = useState<string | null>(null);
|
const [previewKey, setPreviewKey] = useState<string | null>(null);
|
||||||
const [cartRemoveKey, setCartRemoveKey] = useState<string | null>(null);
|
const [cartRemoveKey, setCartRemoveKey] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const variantsRef = useRef(variants);
|
||||||
|
variantsRef.current = variants;
|
||||||
|
|
||||||
const priceMap = useMemo(
|
const priceMap = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Map(
|
new Map(
|
||||||
@ -110,7 +143,11 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
|||||||
[rawMaterials],
|
[rawMaterials],
|
||||||
);
|
);
|
||||||
|
|
||||||
const subtotal = Object.entries(quantities).reduce(
|
const newSubtotal = variants.reduce(
|
||||||
|
(sum, v) => sum + Number(v.price) * Number(v.stock),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const existingSubtotal = Object.entries(quantities).reduce(
|
||||||
(sum, [priceId, quantity]) => {
|
(sum, [priceId, quantity]) => {
|
||||||
const price = priceMap.get(Number(priceId));
|
const price = priceMap.get(Number(priceId));
|
||||||
|
|
||||||
@ -118,8 +155,93 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
|||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
const subtotal = mode === 'existing' ? existingSubtotal : newSubtotal;
|
||||||
const total = subtotal - discount + shippingCost;
|
const total = subtotal - discount + shippingCost;
|
||||||
|
|
||||||
|
const addVariant = useCallback(() => {
|
||||||
|
setVariants((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
variant: '',
|
||||||
|
price: 0,
|
||||||
|
stock: 0,
|
||||||
|
photo: null,
|
||||||
|
photoUrl: null,
|
||||||
|
uploading: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeVariant = useCallback((index: number) => {
|
||||||
|
setVariants((prev) => prev.filter((_, i) => i !== index));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateVariant = useCallback(
|
||||||
|
(index: number, field: keyof VariantState, value: unknown) => {
|
||||||
|
setVariants((prev) => {
|
||||||
|
const updated = [...prev];
|
||||||
|
(updated[index] as Record<string, unknown>)[field] = value;
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
||||||
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||||
|
const [deleteVariantIndex, setDeleteVariantIndex] = useState<number | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirmRemoveVariant = useCallback((index: number) => {
|
||||||
|
setDeleteVariantIndex(index);
|
||||||
|
setDeleteConfirmOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const copyPrice = useCallback((variantIndex: number) => {
|
||||||
|
setVariants((prev) => {
|
||||||
|
const price = prev[variantIndex].price;
|
||||||
|
navigator.clipboard.writeText(String(price));
|
||||||
|
setCopiedIndex(variantIndex);
|
||||||
|
setTimeout(() => setCopiedIndex(null), 1500);
|
||||||
|
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const pastePrice = useCallback((variantIndex: number) => {
|
||||||
|
navigator.clipboard.readText().then((text) => {
|
||||||
|
try {
|
||||||
|
const price = Number(text);
|
||||||
|
|
||||||
|
if (!isNaN(price)) {
|
||||||
|
setVariants((prev) => {
|
||||||
|
const updated = [...prev];
|
||||||
|
updated[variantIndex] = {
|
||||||
|
...updated[variantIndex],
|
||||||
|
price,
|
||||||
|
};
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// invalid clipboard data
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const applyToAll = useCallback((variantIndex: number) => {
|
||||||
|
setVariants((prev) => {
|
||||||
|
const sourcePrice = prev[variantIndex].price;
|
||||||
|
|
||||||
|
return prev.map((v, i) =>
|
||||||
|
i === variantIndex ? v : { ...v, price: sourcePrice },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const selectedMaterial = useMemo(
|
const selectedMaterial = useMemo(
|
||||||
() => rawMaterials.find((m) => m.name === selectedMaterialName) ?? null,
|
() => rawMaterials.find((m) => m.name === selectedMaterialName) ?? null,
|
||||||
[rawMaterials, selectedMaterialName],
|
[rawMaterials, selectedMaterialName],
|
||||||
@ -139,7 +261,20 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
|||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const adjustVariantStock = useCallback((index: number, amount: number) => {
|
||||||
|
setVariants((prev) => {
|
||||||
|
const updated = [...prev];
|
||||||
|
updated[index] = {
|
||||||
|
...updated[index],
|
||||||
|
stock: Math.max(0, Number(updated[index].stock) + amount),
|
||||||
|
};
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const cartItems: CartLine[] = (() => {
|
const cartItems: CartLine[] = (() => {
|
||||||
|
if (mode === 'existing') {
|
||||||
const lines: CartLine[] = [];
|
const lines: CartLine[] = [];
|
||||||
|
|
||||||
for (const [priceId, quantity] of Object.entries(quantities)) {
|
for (const [priceId, quantity] of Object.entries(quantities)) {
|
||||||
@ -166,7 +301,22 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.sort((a, b) => a.title.localeCompare(b.title));
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
return variants
|
||||||
|
.map((v, index): CartLine => ({
|
||||||
|
key: `new-${index}`,
|
||||||
|
photoUrl: v.photoUrl,
|
||||||
|
title: `${name || 'Bahan Baku Baru'} — ${v.variant || `Varian ${index + 1}`}`,
|
||||||
|
subtitle: `${formatCurrency(v.price)} / ${purchase.unit}`,
|
||||||
|
price: v.price,
|
||||||
|
quantity: v.stock,
|
||||||
|
onAdjust: (delta) => adjustVariantStock(index, delta),
|
||||||
|
onSet: (value) => updateVariant(index, 'stock', value),
|
||||||
|
onRemove: () => removeVariant(index),
|
||||||
|
}))
|
||||||
|
.filter((line) => line.quantity > 0);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function formatQuantity(value: number): string {
|
function formatQuantity(value: number): string {
|
||||||
@ -174,13 +324,18 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPayload() {
|
function getPayload() {
|
||||||
return {
|
const base = {
|
||||||
mode: 'existing',
|
mode,
|
||||||
supplier_id: supplierId ? Number(supplierId) : null,
|
supplier_id: supplierId ? Number(supplierId) : null,
|
||||||
discount,
|
discount,
|
||||||
shipping_cost: shippingCost,
|
shipping_cost: shippingCost,
|
||||||
notes: notes || null,
|
notes: notes || null,
|
||||||
photo_key: photo,
|
photo_key: photo,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mode === 'existing') {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
existing_items: Object.entries(quantities)
|
existing_items: Object.entries(quantities)
|
||||||
.map(([priceId, quantity]) => ({
|
.map(([priceId, quantity]) => ({
|
||||||
raw_material_price_id: Number(priceId),
|
raw_material_price_id: Number(priceId),
|
||||||
@ -191,6 +346,19 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
name,
|
||||||
|
variants: variantsRef.current.map((v) => ({
|
||||||
|
id: v.id,
|
||||||
|
variant: v.variant,
|
||||||
|
price: Number(v.price),
|
||||||
|
stock: Number(v.stock),
|
||||||
|
photo_key: v.photo,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title="Edit Belanja" />
|
<Head title="Edit Belanja" />
|
||||||
@ -201,10 +369,10 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
|||||||
Edit Belanja
|
Edit Belanja
|
||||||
</h2>
|
</h2>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={purchaseIndex.url()}>
|
<a href={purchaseIndex.url()}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Kembali
|
Kembali
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -219,6 +387,303 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
|||||||
{({ errors, processing }) => (
|
{({ errors, processing }) => (
|
||||||
<div className="grid gap-6 md:grid-cols-3">
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
<div className="space-y-6 md:col-span-2">
|
<div className="space-y-6 md:col-span-2">
|
||||||
|
<Tabs
|
||||||
|
value={mode}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setMode(value as 'new' | 'existing')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="new">
|
||||||
|
Baru
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="existing">
|
||||||
|
Lama
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent
|
||||||
|
value="new"
|
||||||
|
className="mt-0 space-y-6"
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
Informasi Bahan Baku
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid grid-cols-1 gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="edit-name">
|
||||||
|
Nama Bahan Baku{' '}
|
||||||
|
<span className="text-destructive">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-name"
|
||||||
|
name="name"
|
||||||
|
value={name}
|
||||||
|
disabled
|
||||||
|
placeholder="Masukkan nama bahan baku"
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={errors.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
Varian Bahan Baku
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{variants.map(
|
||||||
|
(variant, variantIndex) => (
|
||||||
|
<div
|
||||||
|
key={variantIndex}
|
||||||
|
className="space-y-4 rounded-lg border p-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<h4 className="font-medium">
|
||||||
|
Varian{' '}
|
||||||
|
{variantIndex +
|
||||||
|
1}
|
||||||
|
</h4>
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
onClick={() =>
|
||||||
|
copyPrice(
|
||||||
|
variantIndex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{copiedIndex ===
|
||||||
|
variantIndex ? (
|
||||||
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Salin
|
||||||
|
Harga
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
onClick={() =>
|
||||||
|
pastePrice(
|
||||||
|
variantIndex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ClipboardPaste className="h-4 w-4" />
|
||||||
|
Tempel
|
||||||
|
Harga
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
onClick={() =>
|
||||||
|
applyToAll(
|
||||||
|
variantIndex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Terapkan
|
||||||
|
ke Semua
|
||||||
|
</Button>
|
||||||
|
{variantIndex >
|
||||||
|
0 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() =>
|
||||||
|
confirmRemoveVariant(
|
||||||
|
variantIndex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>
|
||||||
|
Nama
|
||||||
|
Varian{' '}
|
||||||
|
<span className="text-destructive">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
value={
|
||||||
|
variant.variant
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
e,
|
||||||
|
) =>
|
||||||
|
updateVariant(
|
||||||
|
variantIndex,
|
||||||
|
'variant',
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Contoh: Ukuran L, Warna Merah"
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={
|
||||||
|
errors[
|
||||||
|
`variants.${variantIndex}.variant`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>
|
||||||
|
Harga{' '}
|
||||||
|
<span className="text-destructive">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
<RupiahInput
|
||||||
|
value={
|
||||||
|
variant.price
|
||||||
|
}
|
||||||
|
onValueChange={(
|
||||||
|
val,
|
||||||
|
) =>
|
||||||
|
updateVariant(
|
||||||
|
variantIndex,
|
||||||
|
'price',
|
||||||
|
val,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={
|
||||||
|
errors[
|
||||||
|
`variants.${variantIndex}.price`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>
|
||||||
|
Stok{' '}
|
||||||
|
<span className="text-destructive">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
<NumberInput
|
||||||
|
value={
|
||||||
|
variant.stock
|
||||||
|
}
|
||||||
|
onValueChange={(
|
||||||
|
val,
|
||||||
|
) =>
|
||||||
|
updateVariant(
|
||||||
|
variantIndex,
|
||||||
|
'stock',
|
||||||
|
val,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={
|
||||||
|
errors[
|
||||||
|
`variants.${variantIndex}.stock`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>
|
||||||
|
Foto Varian{' '}
|
||||||
|
<span className="text-destructive">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
<FileUpload
|
||||||
|
value={
|
||||||
|
variant.photo
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
key,
|
||||||
|
) => {
|
||||||
|
updateVariant(
|
||||||
|
variantIndex,
|
||||||
|
'photo',
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
updateVariant(
|
||||||
|
variantIndex,
|
||||||
|
'photoUrl',
|
||||||
|
key
|
||||||
|
? getTemporaryUrl(
|
||||||
|
key,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
folder="raw-material-variant"
|
||||||
|
existingUrl={
|
||||||
|
variant.photoUrl
|
||||||
|
}
|
||||||
|
onUploadingChange={(
|
||||||
|
uploading,
|
||||||
|
) =>
|
||||||
|
updateVariant(
|
||||||
|
variantIndex,
|
||||||
|
'uploading',
|
||||||
|
uploading,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={
|
||||||
|
errors[
|
||||||
|
`variants.${variantIndex}.photo_key`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={addVariant}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Tambah Varian
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent
|
||||||
|
value="existing"
|
||||||
|
className="mt-0 space-y-6"
|
||||||
|
>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
@ -251,7 +716,6 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
|||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
placeholder="Cari bahan baku..."
|
placeholder="Cari bahan baku..."
|
||||||
className="w-full"
|
className="w-full"
|
||||||
disabled
|
|
||||||
/>
|
/>
|
||||||
<ComboboxContent>
|
<ComboboxContent>
|
||||||
<ComboboxEmpty>
|
<ComboboxEmpty>
|
||||||
@ -404,6 +868,8 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6 md:col-span-1">
|
<div className="space-y-6 md:col-span-1">
|
||||||
@ -554,10 +1020,15 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
|||||||
disabled={
|
disabled={
|
||||||
processing ||
|
processing ||
|
||||||
uploading ||
|
uploading ||
|
||||||
|
variants.some(
|
||||||
|
(v) => v.uploading,
|
||||||
|
) ||
|
||||||
!supplierId ||
|
!supplierId ||
|
||||||
Object.values(
|
(mode === 'existing'
|
||||||
|
? Object.values(
|
||||||
quantities,
|
quantities,
|
||||||
).every((q) => q <= 0)
|
).every((q) => q <= 0)
|
||||||
|
: !name)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{processing
|
{processing
|
||||||
@ -713,6 +1184,27 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
|||||||
title={cartItems.find((i) => i.key === previewKey)?.title}
|
title={cartItems.find((i) => i.key === previewKey)?.title}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={deleteConfirmOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setDeleteConfirmOpen(false);
|
||||||
|
setDeleteVariantIndex(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Hapus Varian"
|
||||||
|
description="Apakah Anda yakin ingin menghapus varian ini?"
|
||||||
|
confirmLabel="Hapus"
|
||||||
|
onConfirm={() => {
|
||||||
|
if (deleteVariantIndex !== null) {
|
||||||
|
removeVariant(deleteVariantIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeleteConfirmOpen(false);
|
||||||
|
setDeleteVariantIndex(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={cartRemoveKey !== null}
|
open={cartRemoveKey !== null}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
|
|||||||
@ -1,28 +1,18 @@
|
|||||||
|
import { Head, router } from '@inertiajs/react';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { CardTable } from '@/components/card-table';
|
import { CardTable } from '@/components/card-table';
|
||||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||||
import { FilterPopover } from '@/components/filter-popover';
|
|
||||||
import { useCardTableExpand } from '@/components/hooks/use-card-table-expand';
|
import { useCardTableExpand } from '@/components/hooks/use-card-table-expand';
|
||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
|
||||||
Combobox,
|
|
||||||
ComboboxContent,
|
|
||||||
ComboboxEmpty,
|
|
||||||
ComboboxInput,
|
|
||||||
ComboboxItem,
|
|
||||||
ComboboxList,
|
|
||||||
} from '@/components/ui/combobox';
|
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import {
|
import {
|
||||||
destroy,
|
destroy,
|
||||||
create as purchaseCreate,
|
create as purchaseCreate,
|
||||||
edit as purchaseEdit,
|
|
||||||
index as purchaseIndex,
|
index as purchaseIndex,
|
||||||
|
edit as purchaseEdit,
|
||||||
} from '@/routes/admin/manage/purchases';
|
} from '@/routes/admin/manage/purchases';
|
||||||
import { Head, Link, router } from '@inertiajs/react';
|
|
||||||
import { Plus } from 'lucide-react';
|
|
||||||
import { useMemo, useState } from 'react';
|
|
||||||
import type { Purchase } from './columns';
|
import type { Purchase } from './columns';
|
||||||
import { PurchaseCardRow } from './purchase-card';
|
import { PurchaseCardRow } from './purchase-card';
|
||||||
import { PurchaseItemSubRow } from './purchase-sub-row';
|
import { PurchaseItemSubRow } from './purchase-sub-row';
|
||||||
@ -35,21 +25,9 @@ type Props = {
|
|||||||
per_page: number;
|
per_page: number;
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
suppliers: {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}[];
|
|
||||||
filters: {
|
|
||||||
supplier_id?: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PurchaseIndex({
|
export default function PurchaseIndex({ purchases }: Props) {
|
||||||
purchases,
|
|
||||||
filters,
|
|
||||||
suppliers,
|
|
||||||
}: Props) {
|
|
||||||
const { can } = useCan();
|
|
||||||
const [deleting, setDeleting] = useState<Purchase | null>(null);
|
const [deleting, setDeleting] = useState<Purchase | null>(null);
|
||||||
const expand = useCardTableExpand(true);
|
const expand = useCardTableExpand(true);
|
||||||
|
|
||||||
@ -62,28 +40,14 @@ export default function PurchaseIndex({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
search,
|
search,
|
||||||
filterOpen,
|
|
||||||
setFilterOpen,
|
|
||||||
handlePageChange,
|
handlePageChange,
|
||||||
handlePerPageChange,
|
handlePerPageChange,
|
||||||
handleSearchChange,
|
handleSearchChange,
|
||||||
applyFilter,
|
|
||||||
clearFilters,
|
|
||||||
} = useServerTable({
|
} = useServerTable({
|
||||||
route: () => purchaseIndex.url(),
|
route: () => purchaseIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
|
||||||
filterWithParams: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedSupplier = useMemo(
|
|
||||||
() =>
|
|
||||||
suppliers.find(
|
|
||||||
(s) => String(s.id) === filters.supplier_id,
|
|
||||||
) ?? null,
|
|
||||||
[suppliers, filters.supplier_id],
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
if (!deleting) {
|
if (!deleting) {
|
||||||
return;
|
return;
|
||||||
@ -94,50 +58,6 @@ export default function PurchaseIndex({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterToolbar = (
|
|
||||||
<FilterPopover
|
|
||||||
open={filterOpen}
|
|
||||||
onOpenChange={setFilterOpen}
|
|
||||||
filters={filters}
|
|
||||||
hasActiveFilters={Boolean(filters.supplier_id)}
|
|
||||||
onClear={clearFilters}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label className="text-xs text-muted-foreground">
|
|
||||||
Supplier
|
|
||||||
</label>
|
|
||||||
<Combobox
|
|
||||||
items={suppliers}
|
|
||||||
itemToStringLabel={(supplier) => supplier.name}
|
|
||||||
value={selectedSupplier}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
applyFilter(
|
|
||||||
'supplier_id',
|
|
||||||
value ? String(value.id) : '',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ComboboxInput
|
|
||||||
placeholder="Pilih supplier..."
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<ComboboxContent>
|
|
||||||
<ComboboxEmpty>
|
|
||||||
Tidak ada supplier ditemukan.
|
|
||||||
</ComboboxEmpty>
|
|
||||||
<ComboboxList>
|
|
||||||
{(supplier) => (
|
|
||||||
<ComboboxItem value={supplier}>
|
|
||||||
{supplier.name}
|
|
||||||
</ComboboxItem>
|
|
||||||
)}
|
|
||||||
</ComboboxList>
|
|
||||||
</ComboboxContent>
|
|
||||||
</Combobox>
|
|
||||||
</div>
|
|
||||||
</FilterPopover>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title="Belanja" />
|
<Head title="Belanja" />
|
||||||
@ -146,14 +66,12 @@ export default function PurchaseIndex({
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Belanja"
|
title="Belanja"
|
||||||
actions={
|
actions={
|
||||||
can('purchases.create') ? (
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href={purchaseCreate.url()}>
|
<a href={purchaseCreate.url()}>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Tambah
|
Tambah
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -165,7 +83,6 @@ export default function PurchaseIndex({
|
|||||||
searchValue={search}
|
searchValue={search}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchPlaceholder="Cari berdasarkan supplier..."
|
searchPlaceholder="Cari berdasarkan supplier..."
|
||||||
toolbar={filterToolbar}
|
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onPerPageChange={handlePerPageChange}
|
onPerPageChange={handlePerPageChange}
|
||||||
@ -186,7 +103,7 @@ export default function PurchaseIndex({
|
|||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
onToggleExpand={onToggleExpand}
|
onToggleExpand={onToggleExpand}
|
||||||
onEdit={(p) => {
|
onEdit={(p) => {
|
||||||
router.visit(purchaseEdit.url(p.id));
|
window.location.href = purchaseEdit.url(p.id);
|
||||||
}}
|
}}
|
||||||
onDelete={(p) => setDeleting(p)}
|
onDelete={(p) => setDeleting(p)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { ImagePreviewButton } from '@/components/image-preview-button';
|
|||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { formatDateTime, formatNumber } from '@/lib/format';
|
import { formatDateTime, formatNumber } from '@/lib/format';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import type { Purchase } from './columns';
|
import type { Purchase } from './columns';
|
||||||
@ -25,7 +24,6 @@ export function PurchaseCardRow({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: PurchaseCardRowParams) {
|
}: PurchaseCardRowParams) {
|
||||||
const { can } = useCan();
|
|
||||||
const items = purchase.purchase_items ?? [];
|
const items = purchase.purchase_items ?? [];
|
||||||
const variantCount = items.length;
|
const variantCount = items.length;
|
||||||
const rawMaterialName =
|
const rawMaterialName =
|
||||||
@ -131,7 +129,6 @@ export function PurchaseCardRow({
|
|||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show: can('purchases.update'),
|
|
||||||
onClick: () => onEdit(purchase),
|
onClick: () => onEdit(purchase),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -139,7 +136,6 @@ export function PurchaseCardRow({
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show: can('purchases.delete'),
|
|
||||||
onClick: () => onDelete(purchase),
|
onClick: () => onDelete(purchase),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use no memo';
|
'use no memo';
|
||||||
|
|
||||||
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
import { Form, Head, usePage } from '@inertiajs/react';
|
||||||
import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
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 { ConfirmDialog } from '@/components/confirm-dialog';
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
@ -188,7 +188,7 @@ return 0;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.sort((a, b) => a.title.localeCompare(b.title));
|
return lines;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function formatQuantity(value: number): string {
|
function formatQuantity(value: number): string {
|
||||||
@ -219,10 +219,10 @@ return 0;
|
|||||||
Tambah Restock
|
Tambah Restock
|
||||||
</h2>
|
</h2>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={restockIndex.url()}>
|
<a href={restockIndex.url()}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Kembali
|
Kembali
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use no memo';
|
'use no memo';
|
||||||
|
|
||||||
import { Form, Head, Link } from '@inertiajs/react';
|
import { Form, Head } from '@inertiajs/react';
|
||||||
import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
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 { ConfirmDialog } from '@/components/confirm-dialog';
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
@ -162,7 +162,7 @@ return 0;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.sort((a, b) => a.title.localeCompare(b.title));
|
return lines;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function formatQuantity(value: number): string {
|
function formatQuantity(value: number): string {
|
||||||
@ -193,10 +193,10 @@ return 0;
|
|||||||
Edit Restock
|
Edit Restock
|
||||||
</h2>
|
</h2>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={restockIndex.url()}>
|
<a href={restockIndex.url()}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Kembali
|
Kembali
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Head, Link, router } from '@inertiajs/react';
|
import { Head, router } from '@inertiajs/react';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { CardTable } from '@/components/card-table';
|
import { CardTable } from '@/components/card-table';
|
||||||
@ -6,7 +6,6 @@ import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
|||||||
import { useCardTableExpand } from '@/components/hooks/use-card-table-expand';
|
import { useCardTableExpand } from '@/components/hooks/use-card-table-expand';
|
||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import {
|
import {
|
||||||
destroy,
|
destroy,
|
||||||
@ -29,7 +28,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function RestockIndex({ restocks }: Props) {
|
export default function RestockIndex({ restocks }: Props) {
|
||||||
const { can } = useCan();
|
|
||||||
const [deleting, setDeleting] = useState<Restock | null>(null);
|
const [deleting, setDeleting] = useState<Restock | null>(null);
|
||||||
const expand = useCardTableExpand(true);
|
const expand = useCardTableExpand(true);
|
||||||
|
|
||||||
@ -68,14 +66,12 @@ export default function RestockIndex({ restocks }: Props) {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Restock"
|
title="Restock"
|
||||||
actions={
|
actions={
|
||||||
can('restocks.create') ? (
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href={restockCreate.url()}>
|
<a href={restockCreate.url()}>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Tambah
|
Tambah
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -107,7 +103,7 @@ export default function RestockIndex({ restocks }: Props) {
|
|||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
onToggleExpand={onToggleExpand}
|
onToggleExpand={onToggleExpand}
|
||||||
onEdit={(r) => {
|
onEdit={(r) => {
|
||||||
router.visit(restockEdit.url(r.id));
|
window.location.href = restockEdit.url(r.id);
|
||||||
}}
|
}}
|
||||||
onDelete={(r) => setDeleting(r)}
|
onDelete={(r) => setDeleting(r)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { RowActions } from '@/components/row-actions';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { formatDateTime, formatNumber } from '@/lib/format';
|
import { formatDateTime, formatNumber } from '@/lib/format';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import type { Restock, RestockStockType } from './columns';
|
import type { Restock, RestockStockType } from './columns';
|
||||||
@ -39,7 +38,6 @@ export function RestockCardRow({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: RestockCardRowParams) {
|
}: RestockCardRowParams) {
|
||||||
const { can } = useCan();
|
|
||||||
const items = restock.restock_items ?? [];
|
const items = restock.restock_items ?? [];
|
||||||
const variantCount = items.length;
|
const variantCount = items.length;
|
||||||
const productNames = [
|
const productNames = [
|
||||||
@ -140,7 +138,6 @@ export function RestockCardRow({
|
|||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show: can('restocks.update'),
|
|
||||||
onClick: () => onEdit(restock),
|
onClick: () => onEdit(restock),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -148,7 +145,6 @@ export function RestockCardRow({
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show: can('restocks.delete'),
|
|
||||||
onClick: () => onDelete(restock),
|
onClick: () => onDelete(restock),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use no memo';
|
'use no memo';
|
||||||
|
|
||||||
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
import { Form, Head, usePage } from '@inertiajs/react';
|
||||||
import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
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 { ConfirmDialog } from '@/components/confirm-dialog';
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
@ -28,6 +28,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
@ -35,7 +36,6 @@ import {
|
|||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
} from '@/components/ui/sheet';
|
} from '@/components/ui/sheet';
|
||||||
import { Switch } from '@/components/ui/switch';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { useTransactionDraftSave } from '@/hooks/use-transaction-draft';
|
import { useTransactionDraftSave } from '@/hooks/use-transaction-draft';
|
||||||
import { formatNumber } from '@/lib/format';
|
import { formatNumber } from '@/lib/format';
|
||||||
@ -182,7 +182,6 @@ export default function TransactionCreate({ data }: Props) {
|
|||||||
if (stockType === 'reject') {
|
if (stockType === 'reject') {
|
||||||
return priceTypeOptions.filter((o) => o.value === 'reject');
|
return priceTypeOptions.filter((o) => o.value === 'reject');
|
||||||
}
|
}
|
||||||
|
|
||||||
return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value));
|
return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value));
|
||||||
}, [stockType, priceTypeOptions]);
|
}, [stockType, priceTypeOptions]);
|
||||||
|
|
||||||
@ -300,10 +299,10 @@ export default function TransactionCreate({ data }: Props) {
|
|||||||
Tambah Transaksi
|
Tambah Transaksi
|
||||||
</h2>
|
</h2>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={transactionIndex.url()}>
|
<a href={transactionIndex.url()}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Kembali
|
Kembali
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use no memo';
|
'use no memo';
|
||||||
|
|
||||||
import { Form, Head, Link } from '@inertiajs/react';
|
import { Form, Head } from '@inertiajs/react';
|
||||||
import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
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 { ConfirmDialog } from '@/components/confirm-dialog';
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
@ -28,6 +28,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
@ -35,7 +36,6 @@ import {
|
|||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
} from '@/components/ui/sheet';
|
} from '@/components/ui/sheet';
|
||||||
import { Switch } from '@/components/ui/switch';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { formatNumber } from '@/lib/format';
|
import { formatNumber } from '@/lib/format';
|
||||||
import { getTemporaryUrl } from '@/lib/upload';
|
import { getTemporaryUrl } from '@/lib/upload';
|
||||||
@ -151,7 +151,6 @@ export default function TransactionEdit({ transaction, data }: Props) {
|
|||||||
if (stockType === 'reject') {
|
if (stockType === 'reject') {
|
||||||
return priceTypeOptions.filter((o) => o.value === 'reject');
|
return priceTypeOptions.filter((o) => o.value === 'reject');
|
||||||
}
|
}
|
||||||
|
|
||||||
return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value));
|
return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value));
|
||||||
}, [stockType, priceTypeOptions]);
|
}, [stockType, priceTypeOptions]);
|
||||||
|
|
||||||
@ -277,10 +276,10 @@ export default function TransactionEdit({ transaction, data }: Props) {
|
|||||||
Edit Transaksi
|
Edit Transaksi
|
||||||
</h2>
|
</h2>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={transactionIndex.url()}>
|
<a href={transactionIndex.url()}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Kembali
|
Kembali
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Head, Link, router } from '@inertiajs/react';
|
import { Head, router } from '@inertiajs/react';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { CardTable } from '@/components/card-table';
|
import { CardTable } from '@/components/card-table';
|
||||||
@ -22,7 +22,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import {
|
import {
|
||||||
destroy,
|
destroy,
|
||||||
@ -74,7 +73,6 @@ export default function TransactionIndex({
|
|||||||
filters,
|
filters,
|
||||||
filterOptions,
|
filterOptions,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { can } = useCan();
|
|
||||||
const [deleting, setDeleting] = useState<Transaction | null>(null);
|
const [deleting, setDeleting] = useState<Transaction | null>(null);
|
||||||
const expand = useCardTableExpand(true);
|
const expand = useCardTableExpand(true);
|
||||||
|
|
||||||
@ -322,14 +320,12 @@ export default function TransactionIndex({
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Transaksi"
|
title="Transaksi"
|
||||||
actions={
|
actions={
|
||||||
can('orders.create') ? (
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href={transactionCreate.url()}>
|
<a href={transactionCreate.url()}>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Tambah
|
Tambah
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -362,7 +358,7 @@ export default function TransactionIndex({
|
|||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
onToggleExpand={onToggleExpand}
|
onToggleExpand={onToggleExpand}
|
||||||
onEdit={(t) => {
|
onEdit={(t) => {
|
||||||
router.visit(transactionEdit.url(t.id));
|
window.location.href = transactionEdit.url(t.id);
|
||||||
}}
|
}}
|
||||||
onDelete={(t) => setDeleting(t)}
|
onDelete={(t) => setDeleting(t)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { RowActions } from '@/components/row-actions';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { formatDateTime, formatNumber } from '@/lib/format';
|
import { formatDateTime, formatNumber } from '@/lib/format';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import type { Transaction } from './columns';
|
import type { Transaction } from './columns';
|
||||||
@ -33,7 +32,6 @@ export function TransactionCardRow({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: TransactionCardRowParams) {
|
}: TransactionCardRowParams) {
|
||||||
const { can } = useCan();
|
|
||||||
const items = transaction.order_items ?? [];
|
const items = transaction.order_items ?? [];
|
||||||
const variantCount = items.length;
|
const variantCount = items.length;
|
||||||
const productNames = [
|
const productNames = [
|
||||||
@ -178,7 +176,6 @@ export function TransactionCardRow({
|
|||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show: can('orders.update'),
|
|
||||||
onClick: () => onEdit(transaction),
|
onClick: () => onEdit(transaction),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -186,7 +183,6 @@ export function TransactionCardRow({
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show: can('orders.delete'),
|
|
||||||
onClick: () => onDelete(transaction),
|
onClick: () => onDelete(transaction),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -10,13 +10,12 @@ export type Category = {
|
|||||||
type CreateColumnsParams = {
|
type CreateColumnsParams = {
|
||||||
handleEdit: (category: Category) => void;
|
handleEdit: (category: Category) => void;
|
||||||
handleDeleteClick: (category: Category) => void;
|
handleDeleteClick: (category: Category) => void;
|
||||||
can: (permission: string) => boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createCategoryColumns(
|
export function createCategoryColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<Category>[] {
|
): ColumnDef<Category>[] {
|
||||||
const { handleEdit, handleDeleteClick, can } = params;
|
const { handleEdit, handleDeleteClick } = params;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -44,7 +43,6 @@ export function createCategoryColumns(
|
|||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show: can('categories.update'),
|
|
||||||
onClick: () => handleEdit(category),
|
onClick: () => handleEdit(category),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -52,7 +50,6 @@ export function createCategoryColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show: can('categories.delete'),
|
|
||||||
onClick: () => handleDeleteClick(category),
|
onClick: () => handleDeleteClick(category),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import { PageHeader } from '@/components/page-header';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { useCan } from '@/hooks/use-can';
|
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import {
|
import {
|
||||||
destroy,
|
destroy,
|
||||||
@ -33,7 +32,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function CategoryIndex({ categories, highlight }: Props) {
|
export default function CategoryIndex({ categories, highlight }: Props) {
|
||||||
const { can } = useCan();
|
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<Category | null>(null);
|
const [editing, setEditing] = useState<Category | null>(null);
|
||||||
const [deleting, setDeleting] = useState<Category | null>(null);
|
const [deleting, setDeleting] = useState<Category | null>(null);
|
||||||
@ -68,7 +66,6 @@ export default function CategoryIndex({ categories, highlight }: Props) {
|
|||||||
const columns = createCategoryColumns({
|
const columns = createCategoryColumns({
|
||||||
handleEdit: (category) => setEditing(category),
|
handleEdit: (category) => setEditing(category),
|
||||||
handleDeleteClick: (category) => setDeleting(category),
|
handleDeleteClick: (category) => setDeleting(category),
|
||||||
can,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -101,7 +98,6 @@ export default function CategoryIndex({ categories, highlight }: Props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
can('categories.create') ? (
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -111,7 +107,6 @@ export default function CategoryIndex({ categories, highlight }: Props) {
|
|||||||
Tambah
|
Tambah
|
||||||
</button>
|
</button>
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user