Compare commits
No commits in common. "bc02af2a66f7255e2a0401364a5e5448a66aebdf" and "6ebcbc24c0733c1602a9ac8b4dbb95d2bd32cd67" have entirely different histories.
bc02af2a66
...
6ebcbc24c0
@ -10,16 +10,16 @@ enum CashTransactionType: string
|
|||||||
|
|
||||||
case DEPOSIT = 'deposit';
|
case DEPOSIT = 'deposit';
|
||||||
case EXPENSE = 'expense';
|
case EXPENSE = 'expense';
|
||||||
|
case TRANSFER = 'transfer';
|
||||||
case WITHDRAWAL = 'withdrawal';
|
case WITHDRAWAL = 'withdrawal';
|
||||||
case EMPLOYEE_ADVANCE = 'employee_advance';
|
|
||||||
|
|
||||||
public function label(): string
|
public function label(): string
|
||||||
{
|
{
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
self::DEPOSIT => 'Deposit',
|
self::DEPOSIT => 'Setoran',
|
||||||
self::EXPENSE => 'Pengeluaran',
|
self::EXPENSE => 'Pengeluaran',
|
||||||
self::WITHDRAWAL => 'Withdrawal',
|
self::TRANSFER => 'Transfer',
|
||||||
self::EMPLOYEE_ADVANCE => 'Kasbon',
|
self::WITHDRAWAL => 'Penarikan',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,8 +71,9 @@ enum Permission: string
|
|||||||
case PRODUCTS_UPDATE = 'products.update';
|
case PRODUCTS_UPDATE = 'products.update';
|
||||||
case PRODUCTS_DELETE = 'products.delete';
|
case PRODUCTS_DELETE = 'products.delete';
|
||||||
case PRODUCTS_TOGGLE_STATUS = 'products.toggle_status';
|
case PRODUCTS_TOGGLE_STATUS = 'products.toggle_status';
|
||||||
case PRODUCTS_TRANSFER_STOCK = 'products.transfer_stock';
|
|
||||||
case PRODUCTS_VIEW_STOCK_MUTATIONS = 'products.view_stock_mutations';
|
// Stocks
|
||||||
|
case STOCKS_VIEW = 'stocks.view';
|
||||||
|
|
||||||
// Orders
|
// Orders
|
||||||
case ORDERS_VIEW = 'orders.view';
|
case ORDERS_VIEW = 'orders.view';
|
||||||
@ -112,7 +113,6 @@ enum Permission: string
|
|||||||
case EMPLOYEE_ADVANCES_UPDATE = 'employee_advances.update';
|
case EMPLOYEE_ADVANCES_UPDATE = 'employee_advances.update';
|
||||||
case EMPLOYEE_ADVANCES_DELETE = 'employee_advances.delete';
|
case EMPLOYEE_ADVANCES_DELETE = 'employee_advances.delete';
|
||||||
case EMPLOYEE_ADVANCES_PAY = 'employee_advances.pay';
|
case EMPLOYEE_ADVANCES_PAY = 'employee_advances.pay';
|
||||||
case EMPLOYEE_ADVANCES_VIEW_PAYMENTS = 'employee_advances.view_payments';
|
|
||||||
case EMPLOYEE_ADVANCES_VERIFY = 'employee_advances.verify';
|
case EMPLOYEE_ADVANCES_VERIFY = 'employee_advances.verify';
|
||||||
|
|
||||||
// Payroll
|
// Payroll
|
||||||
|
|||||||
@ -22,38 +22,19 @@ public function index(Request $request): Response
|
|||||||
{
|
{
|
||||||
$year = $request->integer('year', now()->year);
|
$year = $request->integer('year', now()->year);
|
||||||
$month = $request->integer('month', now()->month);
|
$month = $request->integer('month', now()->month);
|
||||||
|
|
||||||
$hrSettings = app(HRSettings::class);
|
$hrSettings = app(HRSettings::class);
|
||||||
$user = auth()->user();
|
|
||||||
$isAdmin = $user->hasAnyRole(['developer', 'owner', 'direktur']);
|
|
||||||
|
|
||||||
if ($isAdmin) {
|
|
||||||
return Inertia::render('admin/hr/attendance/index', [
|
|
||||||
'attendances' => $this->service->getByMonth($year, $month),
|
|
||||||
'todayAttendance' => null,
|
|
||||||
'currentYear' => $year,
|
|
||||||
'currentMonth' => $month,
|
|
||||||
'monthStats' => $this->service->getMonthStats($year, $month),
|
|
||||||
'hrSettings' => [
|
|
||||||
'scheduled_check_in_time' => $hrSettings->scheduled_check_in_time,
|
|
||||||
'scheduled_check_out_time' => $hrSettings->scheduled_check_out_time,
|
|
||||||
],
|
|
||||||
'isAdmin' => true,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$employeeId = $user->employee?->id;
|
|
||||||
|
|
||||||
return Inertia::render('admin/hr/attendance/index', [
|
return Inertia::render('admin/hr/attendance/index', [
|
||||||
'attendances' => $this->service->getByMonth($year, $month, $employeeId),
|
'attendances' => $this->service->getByMonth($year, $month),
|
||||||
'todayAttendance' => $this->service->getToday(),
|
'todayAttendance' => $this->service->getToday(),
|
||||||
'currentYear' => $year,
|
'currentYear' => $year,
|
||||||
'currentMonth' => $month,
|
'currentMonth' => $month,
|
||||||
'monthStats' => $this->service->getMonthStats($year, $month, $employeeId),
|
'monthStats' => $this->service->getMonthStats($year, $month),
|
||||||
'hrSettings' => [
|
'hrSettings' => [
|
||||||
'scheduled_check_in_time' => $hrSettings->scheduled_check_in_time,
|
'scheduled_check_in_time' => $hrSettings->scheduled_check_in_time,
|
||||||
'scheduled_check_out_time' => $hrSettings->scheduled_check_out_time,
|
'scheduled_check_out_time' => $hrSettings->scheduled_check_out_time,
|
||||||
],
|
],
|
||||||
'isAdmin' => false,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,9 +36,7 @@ public function create(): Response
|
|||||||
|
|
||||||
return Inertia::render('admin/hr/employee/create', [
|
return Inertia::render('admin/hr/employee/create', [
|
||||||
'roles' => $this->service->canViewAll()
|
'roles' => $this->service->canViewAll()
|
||||||
? Role::where('name', '!=', 'Developer')
|
? Role::where('name', '!=', 'Developer')->get(['id', 'name'])
|
||||||
->when($this->service->shouldHideAdminBahanBaku(), fn ($q) => $q->where('name', '!=', 'admin-bahan-baku'))
|
|
||||||
->get(['id', 'name'])
|
|
||||||
: Role::where('name', '=', $user->roles->first()?->name)->get(['id', 'name']),
|
: Role::where('name', '=', $user->roles->first()?->name)->get(['id', 'name']),
|
||||||
'canViewAll' => $this->service->canViewAll(),
|
'canViewAll' => $this->service->canViewAll(),
|
||||||
]);
|
]);
|
||||||
@ -59,10 +57,7 @@ public function edit(User $user): Response
|
|||||||
|
|
||||||
return Inertia::render('admin/hr/employee/edit', [
|
return Inertia::render('admin/hr/employee/edit', [
|
||||||
'employee' => $user,
|
'employee' => $user,
|
||||||
'roles' => Role::where('name', '!=', 'Developer')
|
'roles' => Role::where('name', '!=', 'Developer')->get(['id', 'name']),
|
||||||
->when($this->service->shouldHideAdminBahanBaku(), fn ($q) => $q->where('name', '!=', 'admin-bahan-baku'))
|
|
||||||
->get(['id', 'name']),
|
|
||||||
'canViewAll' => $this->service->canViewAll(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,10 +30,10 @@ protected function casts(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function formattedAttendanceDate(): Attribute
|
protected function attendanceDate(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn ($value) => $value ? \Carbon\Carbon::parse($value)->translatedFormat('l, d F Y') : null,
|
get: fn ($value) => $value?->translatedFormat('l, d F Y'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,14 +26,14 @@ protected function casts(): array
|
|||||||
protected function formattedAmount(): Attribute
|
protected function formattedAmount(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn () => 'Rp '.number_format($this->amount, 0, ',', '.'),
|
get: fn() => 'Rp ' . number_format($this->amount, 0, ',', '.'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function formattedPaidAt(): Attribute
|
protected function formattedPaidAt(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn () => $this->paid_at?->translatedFormat('l, d F Y'),
|
get: fn() => $this->paid_at?->translatedFormat('l, d F Y'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -34,14 +34,14 @@ protected function casts(): array
|
|||||||
protected function formattedName(): Attribute
|
protected function formattedName(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn () => ucfirst($this->name),
|
get: fn() => ucfirst($this->name),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function formattedStock(): Attribute
|
protected function formattedStock(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn () => number_format($this->stock, 0, ',', '.'),
|
get: fn() => number_format($this->stock, 0, ',', '.'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
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\EmployeeAdvance;
|
use App\Models\EmployeeAdvance;
|
||||||
use App\Models\EmployeeAdvancePayment;
|
use App\Models\EmployeeAdvancePayment;
|
||||||
@ -48,9 +47,7 @@ public function create(array $data): EmployeeAdvance
|
|||||||
$employee = auth()->user()->employee;
|
$employee = auth()->user()->employee;
|
||||||
|
|
||||||
if (! $employee) {
|
if (! $employee) {
|
||||||
throw ValidationException::withMessages([
|
throw new \Exception('Anda tidak terdaftar sebagai karyawan.');
|
||||||
'amount' => 'Anda tidak terdaftar sebagai karyawan.',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$employeeAdvance = EmployeeAdvance::create([
|
$employeeAdvance = EmployeeAdvance::create([
|
||||||
@ -76,44 +73,6 @@ public function create(array $data): EmployeeAdvance
|
|||||||
|
|
||||||
public function update(EmployeeAdvance $employeeAdvance, array $data): EmployeeAdvance
|
public function update(EmployeeAdvance $employeeAdvance, array $data): EmployeeAdvance
|
||||||
{
|
{
|
||||||
if ($employeeAdvance->status === EmployeeAdvanceStatus::PAID) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'amount' => 'Kasbon yang sudah dibayar tidak dapat diedit.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($employeeAdvance->status === EmployeeAdvanceStatus::APPROVED && $employeeAdvance->cash_transaction_id) {
|
|
||||||
$oldAmount = $employeeAdvance->amount;
|
|
||||||
$newAmount = $data['amount'];
|
|
||||||
|
|
||||||
if ($oldAmount !== $newAmount) {
|
|
||||||
$employeeAdvance = DB::transaction(function () use ($employeeAdvance, $data, $oldAmount, $newAmount) {
|
|
||||||
$this->creditCash(
|
|
||||||
$oldAmount,
|
|
||||||
'Pembatalan kasbon: '.$employeeAdvance->description,
|
|
||||||
CashTransactionType::DEPOSIT,
|
|
||||||
);
|
|
||||||
|
|
||||||
$cashTransaction = $this->debitCash(
|
|
||||||
$newAmount,
|
|
||||||
'Kasbon: '.$data['description'],
|
|
||||||
CashTransactionType::EMPLOYEE_ADVANCE,
|
|
||||||
);
|
|
||||||
|
|
||||||
$employeeAdvance->update([
|
|
||||||
'amount' => $newAmount,
|
|
||||||
'description' => $data['description'],
|
|
||||||
'due_date' => $data['due_date'],
|
|
||||||
'cash_transaction_id' => $cashTransaction->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $employeeAdvance;
|
|
||||||
});
|
|
||||||
|
|
||||||
return $employeeAdvance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$employeeAdvance->update([
|
$employeeAdvance->update([
|
||||||
'amount' => $data['amount'],
|
'amount' => $data['amount'],
|
||||||
'description' => $data['description'],
|
'description' => $data['description'],
|
||||||
@ -125,60 +84,29 @@ public function update(EmployeeAdvance $employeeAdvance, array $data): EmployeeA
|
|||||||
|
|
||||||
public function delete(EmployeeAdvance $employeeAdvance): bool
|
public function delete(EmployeeAdvance $employeeAdvance): bool
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($employeeAdvance) {
|
if ($employeeAdvance->status === EmployeeAdvanceStatus::APPROVED && $employeeAdvance->cash_transaction_id) {
|
||||||
if ($employeeAdvance->status === EmployeeAdvanceStatus::APPROVED && $employeeAdvance->cash_transaction_id) {
|
$cashAccount = $this->getCashAccount();
|
||||||
$this->creditCash(
|
$newBalance = $cashAccount->balance + $employeeAdvance->amount;
|
||||||
$employeeAdvance->amount,
|
$cashAccount->update(['balance' => $newBalance]);
|
||||||
'Pembatalan kasbon: '.$employeeAdvance->description,
|
$employeeAdvance->cashTransaction()->delete();
|
||||||
CashTransactionType::DEPOSIT,
|
}
|
||||||
);
|
|
||||||
$employeeAdvance->cashTransaction()->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($employeeAdvance->status === EmployeeAdvanceStatus::PAID) {
|
return $employeeAdvance->delete();
|
||||||
$this->creditCash(
|
|
||||||
$employeeAdvance->amount,
|
|
||||||
'Pembatalan kasbon: '.$employeeAdvance->description,
|
|
||||||
CashTransactionType::DEPOSIT,
|
|
||||||
);
|
|
||||||
|
|
||||||
$employeeAdvance->load('payments');
|
|
||||||
foreach ($employeeAdvance->payments as $payment) {
|
|
||||||
if ($payment->cash_transaction_id) {
|
|
||||||
$this->debitCash(
|
|
||||||
$payment->amount,
|
|
||||||
'Pembatalan pembayaran kasbon: '.$employeeAdvance->description,
|
|
||||||
CashTransactionType::EXPENSE,
|
|
||||||
);
|
|
||||||
$payment->cashTransaction()->delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$employeeAdvance->payments()->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $employeeAdvance->delete();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance
|
public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance
|
||||||
{
|
{
|
||||||
$employeeAdvance = DB::transaction(function () use ($employeeAdvance) {
|
$cashTransaction = $this->debitCash(
|
||||||
$cashTransaction = $this->debitCash(
|
amount: $employeeAdvance->amount,
|
||||||
$employeeAdvance->amount,
|
description: 'Kasbon: '.$employeeAdvance->description,
|
||||||
'Kasbon: '.$employeeAdvance->description,
|
);
|
||||||
CashTransactionType::EMPLOYEE_ADVANCE
|
|
||||||
);
|
|
||||||
|
|
||||||
$employeeAdvance->update([
|
$employeeAdvance->update([
|
||||||
'cash_transaction_id' => $cashTransaction->id,
|
'cash_transaction_id' => $cashTransaction->id,
|
||||||
'status' => EmployeeAdvanceStatus::APPROVED,
|
'status' => EmployeeAdvanceStatus::APPROVED,
|
||||||
'verified_by_id' => auth()->id(),
|
'verified_by_id' => auth()->id(),
|
||||||
'verified_at' => now(),
|
'verified_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $employeeAdvance;
|
|
||||||
});
|
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||||
@ -203,9 +131,8 @@ public function pay(EmployeeAdvance $employeeAdvance, int $amount): EmployeeAdva
|
|||||||
|
|
||||||
$employeeAdvance = DB::transaction(function () use ($employeeAdvance, $amount) {
|
$employeeAdvance = DB::transaction(function () use ($employeeAdvance, $amount) {
|
||||||
$cashTransaction = $this->creditCash(
|
$cashTransaction = $this->creditCash(
|
||||||
$amount,
|
amount: $amount,
|
||||||
'Pembayaran kasbon: '.$employeeAdvance->description,
|
description: 'Pembayaran kasbon: '.$employeeAdvance->description,
|
||||||
CashTransactionType::DEPOSIT
|
|
||||||
);
|
);
|
||||||
|
|
||||||
EmployeeAdvancePayment::create([
|
EmployeeAdvancePayment::create([
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
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\Expense;
|
use App\Models\Expense;
|
||||||
use App\Services\Concerns\HandlesCashTransactions;
|
use App\Services\Concerns\HandlesCashTransactions;
|
||||||
@ -176,11 +175,10 @@ public function update(Expense $expense, array $data): Expense
|
|||||||
public function delete(Expense $expense): bool
|
public function delete(Expense $expense): bool
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($expense) {
|
return DB::transaction(function () use ($expense) {
|
||||||
$this->creditCash(
|
$cashAccount = CashAccount::firstOrFail();
|
||||||
amount: $expense->amount,
|
|
||||||
description: 'Pembatalan pengeluaran: '.$expense->description,
|
$newBalance = $cashAccount->balance + $expense->amount;
|
||||||
type: CashTransactionType::DEPOSIT,
|
$cashAccount->update(['balance' => $newBalance]);
|
||||||
);
|
|
||||||
|
|
||||||
// Invalidate receipt cache
|
// Invalidate receipt cache
|
||||||
$media = $expense->getFirstMedia('receipts');
|
$media = $expense->getFirstMedia('receipts');
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
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\Payroll;
|
use App\Models\Payroll;
|
||||||
@ -142,10 +141,9 @@ public function pay(Payroll $payroll): Payroll
|
|||||||
}
|
}
|
||||||
|
|
||||||
$payroll = DB::transaction(function () use ($payroll) {
|
$payroll = DB::transaction(function () use ($payroll) {
|
||||||
$cashTransaction = $this->debitCash(
|
$cashTransaction = $this->creditCash(
|
||||||
amount: $payroll->total_amount,
|
amount: $payroll->total_amount,
|
||||||
description: 'Pembayaran gaji karyawan',
|
description: 'Pembayaran gaji karyawan',
|
||||||
type: CashTransactionType::EXPENSE,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$payroll->update([
|
$payroll->update([
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
namespace App\Services\Admin\HR;
|
namespace App\Services\Admin\HR;
|
||||||
|
|
||||||
use App\Models\Attendance;
|
use App\Models\Attendance;
|
||||||
use App\Models\Employee;
|
|
||||||
use App\Models\LeaveRequest;
|
use App\Models\LeaveRequest;
|
||||||
use App\Services\Concerns\RegistersMedia;
|
use App\Services\Concerns\RegistersMedia;
|
||||||
use App\Services\NotificationService;
|
use App\Services\NotificationService;
|
||||||
@ -28,12 +27,11 @@ public function getAll(): Collection
|
|||||||
->map(fn (Attendance $attendance) => $this->formatAttendance($attendance));
|
->map(fn (Attendance $attendance) => $this->formatAttendance($attendance));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getByMonth(int $year, int $month, ?int $employeeId = null): Collection
|
public function getByMonth(int $year, int $month): Collection
|
||||||
{
|
{
|
||||||
return Attendance::with(['employee.user.userProfile', 'media'])
|
return Attendance::with(['employee.user.userProfile', 'media'])
|
||||||
->whereYear('attendance_date', $year)
|
->whereYear('attendance_date', $year)
|
||||||
->whereMonth('attendance_date', $month)
|
->whereMonth('attendance_date', $month)
|
||||||
->when($employeeId, fn ($q) => $q->where('employee_id', $employeeId))
|
|
||||||
->get()
|
->get()
|
||||||
->map(fn (Attendance $attendance) => $this->formatAttendance($attendance));
|
->map(fn (Attendance $attendance) => $this->formatAttendance($attendance));
|
||||||
}
|
}
|
||||||
@ -53,14 +51,7 @@ public function getToday(): ?array
|
|||||||
return $this->getByDate(now()->toDateString());
|
return $this->getByDate(now()->toDateString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAllEmployees(): Collection
|
public function getMonthStats(int $year, int $month): array
|
||||||
{
|
|
||||||
return Employee::with(['user.userProfile', 'user.roles'])
|
|
||||||
->whereHas('user', fn ($q) => $q->where('is_active', true))
|
|
||||||
->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMonthStats(int $year, int $month, ?int $employeeId = null): array
|
|
||||||
{
|
{
|
||||||
$startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth();
|
$startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth();
|
||||||
$endOfMonth = $startOfMonth->copy()->endOfMonth();
|
$endOfMonth = $startOfMonth->copy()->endOfMonth();
|
||||||
@ -74,20 +65,14 @@ public function getMonthStats(int $year, int $month, ?int $employeeId = null): a
|
|||||||
$current->addDay();
|
$current->addDay();
|
||||||
}
|
}
|
||||||
|
|
||||||
$attendanceQuery = Attendance::whereYear('attendance_date', $year)
|
$attendanceCount = Attendance::whereYear('attendance_date', $year)
|
||||||
->whereMonth('attendance_date', $month);
|
->whereMonth('attendance_date', $month)
|
||||||
if ($employeeId) {
|
->count();
|
||||||
$attendanceQuery->where('employee_id', $employeeId);
|
|
||||||
}
|
|
||||||
$attendanceCount = $attendanceQuery->count();
|
|
||||||
|
|
||||||
$leaveQuery = LeaveRequest::approved()
|
$leaveDays = LeaveRequest::approved()
|
||||||
->where('start_date', '<=', $endOfMonth)
|
->where('start_date', '<=', $endOfMonth)
|
||||||
->where('end_date', '>=', $startOfMonth);
|
->where('end_date', '>=', $startOfMonth)
|
||||||
if ($employeeId) {
|
->get()
|
||||||
$leaveQuery->where('employee_id', $employeeId);
|
|
||||||
}
|
|
||||||
$leaveDays = $leaveQuery->get()
|
|
||||||
->reduce(function ($carry, $leave) use ($startOfMonth, $endOfMonth) {
|
->reduce(function ($carry, $leave) use ($startOfMonth, $endOfMonth) {
|
||||||
$leaveStart = max($leave->start_date->timestamp, $startOfMonth->timestamp);
|
$leaveStart = max($leave->start_date->timestamp, $startOfMonth->timestamp);
|
||||||
$leaveEnd = min($leave->end_date->timestamp, $endOfMonth->timestamp);
|
$leaveEnd = min($leave->end_date->timestamp, $endOfMonth->timestamp);
|
||||||
@ -169,7 +154,6 @@ public function checkOut(Attendance $attendance, array $data): Attendance
|
|||||||
private function formatAttendance(Attendance $attendance): array
|
private function formatAttendance(Attendance $attendance): array
|
||||||
{
|
{
|
||||||
$toArray = $attendance->toArray();
|
$toArray = $attendance->toArray();
|
||||||
$toArray['employee_name'] = $attendance->employee?->user?->userProfile?->full_name ?? '-';
|
|
||||||
|
|
||||||
if (is_null($toArray['work_duration_minutes']) && $attendance->check_in_at) {
|
if (is_null($toArray['work_duration_minutes']) && $attendance->check_in_at) {
|
||||||
$checkIn = Carbon::parse($attendance->check_in_at);
|
$checkIn = Carbon::parse($attendance->check_in_at);
|
||||||
|
|||||||
@ -10,18 +10,12 @@
|
|||||||
class EmployeeService
|
class EmployeeService
|
||||||
{
|
{
|
||||||
private const ADMIN_ROLES = ['developer', 'owner', 'direktur', 'admin-toko'];
|
private const ADMIN_ROLES = ['developer', 'owner', 'direktur', 'admin-toko'];
|
||||||
private const RESTRICTED_ROLES = ['admin-toko', 'direktur'];
|
|
||||||
|
|
||||||
public function canViewAll(): bool
|
public function canViewAll(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()->hasAnyRole(self::ADMIN_ROLES);
|
return auth()->user()->hasAnyRole(self::ADMIN_ROLES);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function shouldHideAdminBahanBaku(): bool
|
|
||||||
{
|
|
||||||
return auth()->user()->hasAnyRole(self::RESTRICTED_ROLES);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
public function getAll(array $filters = []): Collection
|
||||||
{
|
{
|
||||||
return User::select(['id', 'email', 'username', 'is_active'])
|
return User::select(['id', 'email', 'username', 'is_active'])
|
||||||
@ -35,7 +29,6 @@ public function getAll(array $filters = []): Collection
|
|||||||
$userRoles = auth()->user()->roles->pluck('name');
|
$userRoles = auth()->user()->roles->pluck('name');
|
||||||
$q->whereHas('roles', fn ($rq) => $rq->whereIn('name', $userRoles));
|
$q->whereHas('roles', fn ($rq) => $rq->whereIn('name', $userRoles));
|
||||||
})
|
})
|
||||||
->when($this->shouldHideAdminBahanBaku(), fn ($q) => $q->whereDoesntHave('roles', fn ($rq) => $rq->where('name', 'admin-bahan-baku')))
|
|
||||||
->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)))
|
||||||
->when($filters['gender'] ?? null, fn ($q, $gender) => $q->whereHas('userProfile', fn ($uq) => $uq->where('gender', $gender)))
|
->when($filters['gender'] ?? null, fn ($q, $gender) => $q->whereHas('userProfile', fn ($uq) => $uq->where('gender', $gender)))
|
||||||
@ -57,7 +50,6 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
$userRoles = auth()->user()->roles->pluck('name');
|
$userRoles = auth()->user()->roles->pluck('name');
|
||||||
$q->whereHas('roles', fn ($rq) => $rq->whereIn('name', $userRoles));
|
$q->whereHas('roles', fn ($rq) => $rq->whereIn('name', $userRoles));
|
||||||
})
|
})
|
||||||
->when($this->shouldHideAdminBahanBaku(), fn ($q) => $q->whereDoesntHave('roles', fn ($rq) => $rq->where('name', 'admin-bahan-baku')))
|
|
||||||
->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)))
|
||||||
->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)))
|
||||||
|
|||||||
@ -12,16 +12,10 @@
|
|||||||
|
|
||||||
class LeaveRequestService
|
class LeaveRequestService
|
||||||
{
|
{
|
||||||
private function canViewAll(): bool
|
|
||||||
{
|
|
||||||
return auth()->user()->hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
|
||||||
->when($filters['status'] ?? null, function ($query, $status) {
|
->when($filters['status'] ?? null, function ($query, $status) {
|
||||||
$query->where('status', $status);
|
$query->where('status', $status);
|
||||||
})
|
})
|
||||||
@ -34,7 +28,6 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
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(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
|
||||||
->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) {
|
||||||
$query->where('status', $status);
|
$query->where('status', $status);
|
||||||
|
|||||||
@ -30,14 +30,14 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
->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' => fn($q) => $q
|
||||||
->select(['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)'),
|
->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',
|
||||||
])
|
])
|
||||||
->when($search, function ($q) use ($search) {
|
->when($search, function ($q) use ($search) {
|
||||||
$q->whereHas('restockItems.productVariant.product', fn ($sq) => $sq->where('name', 'like', "%{$search}%"))
|
$q->whereHas('restockItems.productVariant.product', fn($sq) => $sq->where('name', 'like', "%{$search}%"))
|
||||||
->orWhere('notes', 'like', "%{$search}%");
|
->orWhere('notes', 'like', "%{$search}%");
|
||||||
})
|
})
|
||||||
->orderBy($sort, $direction)
|
->orderBy($sort, $direction)
|
||||||
@ -79,11 +79,11 @@ public function getForCreate(): array
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
$capitalPrice = $variant->productPrices
|
$capitalPrice = $variant->productPrices
|
||||||
->first(fn ($price) => $price->type === PriceType::CAPITAL);
|
->first(fn($price) => $price->type === PriceType::CAPITAL);
|
||||||
$variant->capital_price = $capitalPrice?->price ?? 0;
|
$variant->capital_price = $capitalPrice?->price ?? 0;
|
||||||
|
|
||||||
$rejectPrice = $variant->productPrices
|
$rejectPrice = $variant->productPrices
|
||||||
->first(fn ($price) => $price->type === PriceType::REJECT);
|
->first(fn($price) => $price->type === PriceType::REJECT);
|
||||||
$variant->reject_price = $rejectPrice?->price ?? 0;
|
$variant->reject_price = $rejectPrice?->price ?? 0;
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
@ -93,7 +93,7 @@ public function getForCreate(): array
|
|||||||
public function getForEdit(Restock $restock): array
|
public function getForEdit(Restock $restock): array
|
||||||
{
|
{
|
||||||
$restock->load([
|
$restock->load([
|
||||||
'restockItems' => fn ($q) => $q
|
'restockItems' => fn($q) => $q
|
||||||
->orderByRaw('(SELECT name FROM product_variants WHERE product_variants.id = restock_items.product_variant_id)'),
|
->orderByRaw('(SELECT name FROM product_variants WHERE product_variants.id = restock_items.product_variant_id)'),
|
||||||
'restockItems.productVariant.product',
|
'restockItems.productVariant.product',
|
||||||
]);
|
]);
|
||||||
@ -108,7 +108,7 @@ public function getForEdit(Restock $restock): array
|
|||||||
'photo_url' => $media
|
'photo_url' => $media
|
||||||
? $this->s3Service->getTemporaryUrl($media->file_name)
|
? $this->s3Service->getTemporaryUrl($media->file_name)
|
||||||
: null,
|
: null,
|
||||||
'items' => $restock->restockItems->map(fn (RestockItem $item) => [
|
'items' => $restock->restockItems->map(fn(RestockItem $item) => [
|
||||||
'id' => $item->id,
|
'id' => $item->id,
|
||||||
'product_variant_id' => $item->product_variant_id,
|
'product_variant_id' => $item->product_variant_id,
|
||||||
'quantity' => $item->quantity,
|
'quantity' => $item->quantity,
|
||||||
@ -145,7 +145,7 @@ public function create(array $data): Restock
|
|||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Admin Toko'],
|
roles: ['Owner', 'Developer', 'Admin Toko'],
|
||||||
title: 'Restock Baru',
|
title: 'Restock Baru',
|
||||||
body: 'Restock '.($stockType === ProductStockQuality::GOOD->value ? 'produk' : 'reject').' sebesar Rp '.number_format($subtotal, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
|
body: 'Restock ' . ($stockType === ProductStockQuality::GOOD->value ? 'produk' : 'reject') . ' sebesar Rp ' . number_format($subtotal, 0, ',', '.') . ' berhasil dicatat oleh ' . auth()->user()->full_name . '.',
|
||||||
url: route('admin.manage.restocks.index'),
|
url: route('admin.manage.restocks.index'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -219,7 +219,7 @@ private function buildItemRows(array $items, string $stockType, $now, int &$subt
|
|||||||
->get()
|
->get()
|
||||||
->mapWithKeys(function (ProductVariant $variant) use ($priceType) {
|
->mapWithKeys(function (ProductVariant $variant) use ($priceType) {
|
||||||
$price = $variant->productPrices
|
$price = $variant->productPrices
|
||||||
->first(fn ($p) => $p->type === $priceType);
|
->first(fn($p) => $p->type === $priceType);
|
||||||
|
|
||||||
return [$variant->id => $price?->price ?? 0];
|
return [$variant->id => $price?->price ?? 0];
|
||||||
});
|
});
|
||||||
|
|||||||
@ -53,18 +53,18 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
'orderItems.productVariant.product:id,name',
|
'orderItems.productVariant.product:id,name',
|
||||||
])
|
])
|
||||||
->when($search, function ($q) use ($search) {
|
->when($search, function ($q) use ($search) {
|
||||||
$q->whereHas('orderItems.productVariant.product', fn ($sq) => $sq->where('name', 'like', "%{$search}%"))
|
$q->whereHas('orderItems.productVariant.product', fn($sq) => $sq->where('name', 'like', "%{$search}%"))
|
||||||
->orWhere('order_number', 'like', "%{$search}%")
|
->orWhere('order_number', 'like', "%{$search}%")
|
||||||
->orWhere('notes', 'like', "%{$search}%");
|
->orWhere('notes', 'like', "%{$search}%");
|
||||||
})
|
})
|
||||||
->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
|
->when($filters['status'] ?? null, fn($q, $status) => $q->where('status', $status))
|
||||||
->when($filters['channel'] ?? null, fn ($q, $channel) => $q->where('channel', $channel))
|
->when($filters['channel'] ?? null, fn($q, $channel) => $q->where('channel', $channel))
|
||||||
->when($filters['payment_type'] ?? null, fn ($q, $paymentType) => $q->where('payment_type', $paymentType))
|
->when($filters['payment_type'] ?? null, fn($q, $paymentType) => $q->where('payment_type', $paymentType))
|
||||||
->when($filters['customer_id'] ?? null, fn ($q, $customerId) => $q->where('customer_id', $customerId))
|
->when($filters['customer_id'] ?? null, fn($q, $customerId) => $q->where('customer_id', $customerId))
|
||||||
->when($filters['marketing_id'] ?? null, fn ($q, $marketingId) => $q->where('marketing_id', $marketingId))
|
->when($filters['marketing_id'] ?? null, fn($q, $marketingId) => $q->where('marketing_id', $marketingId))
|
||||||
->when($filters['created_by_id'] ?? null, fn ($q, $createdById) => $q->where('created_by_id', $createdById))
|
->when($filters['created_by_id'] ?? null, fn($q, $createdById) => $q->where('created_by_id', $createdById))
|
||||||
->when($filters['date_from'] ?? null, fn ($q, $dateFrom) => $q->whereDate('created_at', '>=', $dateFrom))
|
->when($filters['date_from'] ?? null, fn($q, $dateFrom) => $q->whereDate('created_at', '>=', $dateFrom))
|
||||||
->when($filters['date_to'] ?? null, fn ($q, $dateTo) => $q->whereDate('created_at', '<=', $dateTo))
|
->when($filters['date_to'] ?? null, fn($q, $dateTo) => $q->whereDate('created_at', '<=', $dateTo))
|
||||||
->orderBy($sort, $direction)
|
->orderBy($sort, $direction)
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
|
|
||||||
@ -99,14 +99,14 @@ public function getSummary(array $filters = []): array
|
|||||||
->selectRaw('COALESCE(SUM(subtotal) - SUM(COALESCE(nego_price, subtotal)), 0) as total_discount')
|
->selectRaw('COALESCE(SUM(subtotal) - SUM(COALESCE(nego_price, subtotal)), 0) as total_discount')
|
||||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total_amount')
|
->selectRaw('COALESCE(SUM(total_amount), 0) as total_amount')
|
||||||
->selectRaw('COALESCE(SUM(cogs), 0) as total_cogs')
|
->selectRaw('COALESCE(SUM(cogs), 0) as total_cogs')
|
||||||
->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
|
->when($filters['status'] ?? null, fn($q, $status) => $q->where('status', $status))
|
||||||
->when($filters['channel'] ?? null, fn ($q, $channel) => $q->where('channel', $channel))
|
->when($filters['channel'] ?? null, fn($q, $channel) => $q->where('channel', $channel))
|
||||||
->when($filters['payment_type'] ?? null, fn ($q, $paymentType) => $q->where('payment_type', $paymentType))
|
->when($filters['payment_type'] ?? null, fn($q, $paymentType) => $q->where('payment_type', $paymentType))
|
||||||
->when($filters['customer_id'] ?? null, fn ($q, $customerId) => $q->where('customer_id', $customerId))
|
->when($filters['customer_id'] ?? null, fn($q, $customerId) => $q->where('customer_id', $customerId))
|
||||||
->when($filters['marketing_id'] ?? null, fn ($q, $marketingId) => $q->where('marketing_id', $marketingId))
|
->when($filters['marketing_id'] ?? null, fn($q, $marketingId) => $q->where('marketing_id', $marketingId))
|
||||||
->when($filters['created_by_id'] ?? null, fn ($q, $createdById) => $q->where('created_by_id', $createdById))
|
->when($filters['created_by_id'] ?? null, fn($q, $createdById) => $q->where('created_by_id', $createdById))
|
||||||
->when($filters['date_from'] ?? null, fn ($q, $dateFrom) => $q->whereDate('created_at', '>=', $dateFrom))
|
->when($filters['date_from'] ?? null, fn($q, $dateFrom) => $q->whereDate('created_at', '>=', $dateFrom))
|
||||||
->when($filters['date_to'] ?? null, fn ($q, $dateTo) => $q->whereDate('created_at', '<=', $dateTo))
|
->when($filters['date_to'] ?? null, fn($q, $dateTo) => $q->whereDate('created_at', '<=', $dateTo))
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -134,9 +134,9 @@ public function getFilterOptions(): array
|
|||||||
->with('userProfile:id,user_id,full_name')
|
->with('userProfile:id,user_id,full_name')
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->get()
|
->get()
|
||||||
->filter(fn (User $user) => $user->userProfile?->full_name)
|
->filter(fn(User $user) => $user->userProfile?->full_name)
|
||||||
->values()
|
->values()
|
||||||
->map(fn (User $user) => [
|
->map(fn(User $user) => [
|
||||||
'id' => $user->id,
|
'id' => $user->id,
|
||||||
'name' => $user->userProfile->full_name,
|
'name' => $user->userProfile->full_name,
|
||||||
]),
|
]),
|
||||||
@ -162,7 +162,7 @@ public function getForCreate(): array
|
|||||||
? $this->s3Service->getTemporaryUrl($media->file_name)
|
? $this->s3Service->getTemporaryUrl($media->file_name)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
$prices = $variant->productPrices->mapWithKeys(fn ($p) => [$p->type->value => $p->price]);
|
$prices = $variant->productPrices->mapWithKeys(fn($p) => [$p->type->value => $p->price]);
|
||||||
$variant->prices = $prices;
|
$variant->prices = $prices;
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
@ -176,11 +176,11 @@ public function getForCreate(): array
|
|||||||
->with('userProfile:id,user_id,full_name')
|
->with('userProfile:id,user_id,full_name')
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->get()
|
->get()
|
||||||
->filter(fn (User $user) => $user->userProfile?->full_name)
|
->filter(fn(User $user) => $user->userProfile?->full_name)
|
||||||
->values(),
|
->values(),
|
||||||
'channelOptions' => OrderChannel::toSelect(),
|
'channelOptions' => OrderChannel::toSelect(),
|
||||||
'paymentTypeOptions' => PaymentType::toSelect(),
|
'paymentTypeOptions' => PaymentType::toSelect(),
|
||||||
'priceTypeOptions' => PriceType::toSelect()->filter(fn ($p) => $p['value'] !== PriceType::CAPITAL->value)->values(),
|
'priceTypeOptions' => PriceType::toSelect()->filter(fn($p) => $p['value'] !== PriceType::CAPITAL->value)->values(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,7 +210,7 @@ public function getForEdit(Order $order): array
|
|||||||
'photo_url' => $media
|
'photo_url' => $media
|
||||||
? $this->s3Service->getTemporaryUrl($media->file_name)
|
? $this->s3Service->getTemporaryUrl($media->file_name)
|
||||||
: null,
|
: null,
|
||||||
'items' => $order->orderItems->map(fn (OrderItem $item) => [
|
'items' => $order->orderItems->map(fn(OrderItem $item) => [
|
||||||
'id' => $item->id,
|
'id' => $item->id,
|
||||||
'product_variant_id' => $item->product_variant_id,
|
'product_variant_id' => $item->product_variant_id,
|
||||||
'quantity' => $item->quantity,
|
'quantity' => $item->quantity,
|
||||||
@ -270,7 +270,7 @@ public function create(array $data): Order
|
|||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Admin Toko'],
|
roles: ['Owner', 'Developer', 'Admin Toko'],
|
||||||
title: 'Transaksi Baru',
|
title: 'Transaksi Baru',
|
||||||
body: 'Transaksi '.$order->order_number.' sebesar Rp '.number_format($totalAmount, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
|
body: 'Transaksi ' . $order->order_number . ' sebesar Rp ' . number_format($totalAmount, 0, ',', '.') . ' berhasil dicatat oleh ' . auth()->user()->full_name . '.',
|
||||||
url: route('admin.manage.transactions.index'),
|
url: route('admin.manage.transactions.index'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -379,14 +379,14 @@ private function buildItemRows(array $items, string $stockType, string $priceTyp
|
|||||||
|
|
||||||
$prices = $variants->mapWithKeys(function (ProductVariant $variant) use ($resolvedPriceType) {
|
$prices = $variants->mapWithKeys(function (ProductVariant $variant) use ($resolvedPriceType) {
|
||||||
$price = $variant->productPrices
|
$price = $variant->productPrices
|
||||||
->first(fn ($p) => $p->type === $resolvedPriceType);
|
->first(fn($p) => $p->type === $resolvedPriceType);
|
||||||
|
|
||||||
return [$variant->id => $price?->price ?? 0];
|
return [$variant->id => $price?->price ?? 0];
|
||||||
});
|
});
|
||||||
|
|
||||||
$capitalPrices = $variants->mapWithKeys(function (ProductVariant $variant) {
|
$capitalPrices = $variants->mapWithKeys(function (ProductVariant $variant) {
|
||||||
$price = $variant->productPrices
|
$price = $variant->productPrices
|
||||||
->first(fn ($p) => $p->type === PriceType::CAPITAL);
|
->first(fn($p) => $p->type === PriceType::CAPITAL);
|
||||||
|
|
||||||
return [$variant->id => $price?->price ?? 0];
|
return [$variant->id => $price?->price ?? 0];
|
||||||
});
|
});
|
||||||
@ -429,6 +429,6 @@ private function generateOrderNumber(): string
|
|||||||
$sequence = 1;
|
$sequence = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $prefix.$date.str_pad($sequence, 4, '0', STR_PAD_LEFT);
|
return $prefix . $date . str_pad($sequence, 4, '0', STR_PAD_LEFT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,58 +3,11 @@
|
|||||||
namespace App\Services\Concerns;
|
namespace App\Services\Concerns;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||||
|
|
||||||
trait RegistersMedia
|
trait RegistersMedia
|
||||||
{
|
{
|
||||||
private function registerMediaFromBase64(
|
|
||||||
Model $model,
|
|
||||||
string $base64Data,
|
|
||||||
string $collectionName,
|
|
||||||
string $folder,
|
|
||||||
): void {
|
|
||||||
$dataUrl = $base64Data;
|
|
||||||
$mime = 'image/jpeg';
|
|
||||||
|
|
||||||
if (preg_match('/^data:(.+?);base64,/', $dataUrl, $matches)) {
|
|
||||||
$mime = $matches[1];
|
|
||||||
$dataUrl = preg_replace('/^data:.+?;base64,/', '', $dataUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
$binary = base64_decode($dataUrl, true);
|
|
||||||
|
|
||||||
if ($binary === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$extension = match ($mime) {
|
|
||||||
'image/png' => 'png',
|
|
||||||
'image/webp' => 'webp',
|
|
||||||
'image/gif' => 'gif',
|
|
||||||
default => 'jpg',
|
|
||||||
};
|
|
||||||
|
|
||||||
$date = now()->format('Y/m/d');
|
|
||||||
$uuid = Str::uuid();
|
|
||||||
$key = "{$folder}/{$date}/{$uuid}/photo.{$extension}";
|
|
||||||
|
|
||||||
Storage::disk('s3')->put($key, $binary, [
|
|
||||||
'ContentType' => $mime,
|
|
||||||
'ACL' => 'public-read',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->registerMedia(
|
|
||||||
model: $model,
|
|
||||||
s3Key: $key,
|
|
||||||
collectionName: $collectionName,
|
|
||||||
mimeType: $mime,
|
|
||||||
fileSize: strlen($binary),
|
|
||||||
orderColumn: 1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function registerMedia(
|
private function registerMedia(
|
||||||
Model $model,
|
Model $model,
|
||||||
string $s3Key,
|
string $s3Key,
|
||||||
|
|||||||
@ -21,14 +21,14 @@ public function run(): void
|
|||||||
'leave_requests' => ['view', 'create', 'update', 'delete', 'verify'],
|
'leave_requests' => ['view', 'create', 'update', 'delete', 'verify'],
|
||||||
'categories' => ['view', 'create', 'update', 'delete'],
|
'categories' => ['view', 'create', 'update', 'delete'],
|
||||||
'customers' => ['view', 'create', 'update', 'delete'],
|
'customers' => ['view', 'create', 'update', 'delete'],
|
||||||
'products' => ['view', 'create', 'update', 'delete', 'toggle_status', 'transfer_stock', 'view_stock_mutations'],
|
'products' => ['view', 'create', 'update', 'delete', 'toggle_status'],
|
||||||
'stocks' => ['view'],
|
'stocks' => ['view'],
|
||||||
'orders' => ['view', 'create', 'update', 'delete', 'send', 'complete', 'cancel'],
|
'orders' => ['view', 'create', 'update', 'delete', 'send', 'complete', 'cancel'],
|
||||||
'cuttings' => ['view', 'create', 'update', 'delete', 'complete'],
|
'cuttings' => ['view', 'create', 'update', 'delete', 'complete'],
|
||||||
'cash' => ['view', 'deposit', 'withdraw', 'update', 'delete'],
|
'cash' => ['view', 'deposit', 'withdraw', 'update', 'delete'],
|
||||||
'expenses' => ['view', 'create', 'update', 'delete'],
|
'expenses' => ['view', 'create', 'update', 'delete'],
|
||||||
'activity_logs' => ['view'],
|
'activity_logs' => ['view'],
|
||||||
'employee_advances' => ['view', 'create', 'update', 'delete', 'pay', 'view_payments', 'verify'],
|
'employee_advances' => ['view', 'create', 'update', 'delete', 'pay', 'verify'],
|
||||||
'payroll' => ['view', 'pay', 'cancel', 'adjust'],
|
'payroll' => ['view', 'pay', 'cancel', 'adjust'],
|
||||||
'owner_verifications' => ['view', 'verify', 'reject'],
|
'owner_verifications' => ['view', 'verify', 'reject'],
|
||||||
'restocks' => ['view', 'create', 'update', 'delete'],
|
'restocks' => ['view', 'create', 'update', 'delete'],
|
||||||
@ -60,7 +60,7 @@ public function run(): void
|
|||||||
|
|
||||||
$developerOwnerPerms = array_values(array_filter(
|
$developerOwnerPerms = array_values(array_filter(
|
||||||
$allPermissions,
|
$allPermissions,
|
||||||
fn($p) => ! in_array($p, $excludedFromDeveloperOwner, true)
|
fn ($p) => ! in_array($p, $excludedFromDeveloperOwner, true)
|
||||||
));
|
));
|
||||||
|
|
||||||
$rolePermissions = [
|
$rolePermissions = [
|
||||||
@ -92,6 +92,7 @@ public function run(): void
|
|||||||
'analysis.top_products',
|
'analysis.top_products',
|
||||||
'analysis.marketing_sales',
|
'analysis.marketing_sales',
|
||||||
|
|
||||||
|
'stocks.view',
|
||||||
'stok_opnames.view',
|
'stok_opnames.view',
|
||||||
|
|
||||||
'attendances.view',
|
'attendances.view',
|
||||||
@ -123,8 +124,6 @@ public function run(): void
|
|||||||
'products.update',
|
'products.update',
|
||||||
'products.delete',
|
'products.delete',
|
||||||
'products.toggle_status',
|
'products.toggle_status',
|
||||||
'products.transfer_stock',
|
|
||||||
'products.view_stock_mutations',
|
|
||||||
|
|
||||||
'owner_verifications.view',
|
'owner_verifications.view',
|
||||||
|
|
||||||
@ -154,12 +153,9 @@ public function run(): void
|
|||||||
'employee_advances.update',
|
'employee_advances.update',
|
||||||
'employee_advances.delete',
|
'employee_advances.delete',
|
||||||
'employee_advances.pay',
|
'employee_advances.pay',
|
||||||
'employee_advances.view_payments',
|
|
||||||
|
|
||||||
'payroll.view',
|
'payroll.view',
|
||||||
'payroll.adjust',
|
'payroll.adjust',
|
||||||
'payroll.pay',
|
|
||||||
'payroll.cancel',
|
|
||||||
|
|
||||||
'settings.view_system',
|
'settings.view_system',
|
||||||
'settings.update_system',
|
'settings.update_system',
|
||||||
@ -224,9 +220,9 @@ public function run(): void
|
|||||||
'employee_advances.update',
|
'employee_advances.update',
|
||||||
'employee_advances.delete',
|
'employee_advances.delete',
|
||||||
'employee_advances.pay',
|
'employee_advances.pay',
|
||||||
'employee_advances.view_payments',
|
|
||||||
|
|
||||||
'payroll.view',
|
'payroll.view',
|
||||||
|
'payroll.adjust',
|
||||||
], true);
|
], true);
|
||||||
})),
|
})),
|
||||||
|
|
||||||
@ -254,6 +250,7 @@ public function run(): void
|
|||||||
'raw_materials.create',
|
'raw_materials.create',
|
||||||
'raw_materials.update',
|
'raw_materials.update',
|
||||||
'raw_materials.toggle_status',
|
'raw_materials.toggle_status',
|
||||||
|
'stocks.view',
|
||||||
|
|
||||||
'owner_verifications.view',
|
'owner_verifications.view',
|
||||||
|
|
||||||
@ -311,7 +308,6 @@ public function run(): void
|
|||||||
'employee_advances.update',
|
'employee_advances.update',
|
||||||
'employee_advances.delete',
|
'employee_advances.delete',
|
||||||
'employee_advances.pay',
|
'employee_advances.pay',
|
||||||
'employee_advances.view_payments',
|
|
||||||
|
|
||||||
'payroll.view',
|
'payroll.view',
|
||||||
], true);
|
], true);
|
||||||
@ -350,7 +346,6 @@ public function run(): void
|
|||||||
'employee_advances.update',
|
'employee_advances.update',
|
||||||
'employee_advances.delete',
|
'employee_advances.delete',
|
||||||
'employee_advances.pay',
|
'employee_advances.pay',
|
||||||
'employee_advances.view_payments',
|
|
||||||
|
|
||||||
'payroll.view',
|
'payroll.view',
|
||||||
], true);
|
], true);
|
||||||
@ -366,7 +361,6 @@ public function run(): void
|
|||||||
'dashboard.orders_payment',
|
'dashboard.orders_payment',
|
||||||
'dashboard.orders_marketing',
|
'dashboard.orders_marketing',
|
||||||
'dashboard.orders_status',
|
'dashboard.orders_status',
|
||||||
|
|
||||||
'analysis.view',
|
'analysis.view',
|
||||||
'analysis.attendance',
|
'analysis.attendance',
|
||||||
'analysis.cash',
|
'analysis.cash',
|
||||||
@ -381,38 +375,29 @@ public function run(): void
|
|||||||
'analysis.top_customers',
|
'analysis.top_customers',
|
||||||
'analysis.top_products',
|
'analysis.top_products',
|
||||||
'analysis.marketing_sales',
|
'analysis.marketing_sales',
|
||||||
|
|
||||||
'employees.view',
|
'employees.view',
|
||||||
|
|
||||||
'attendances.view',
|
'attendances.view',
|
||||||
'attendances.create',
|
'attendances.create',
|
||||||
'attendances.delete',
|
'attendances.delete',
|
||||||
'attendances.manage',
|
'attendances.manage',
|
||||||
|
|
||||||
'leave_requests.view',
|
'leave_requests.view',
|
||||||
'leave_requests.create',
|
'leave_requests.create',
|
||||||
'leave_requests.update',
|
'leave_requests.update',
|
||||||
'leave_requests.delete',
|
'leave_requests.delete',
|
||||||
|
|
||||||
'categories.view',
|
'categories.view',
|
||||||
|
|
||||||
'customers.view',
|
'customers.view',
|
||||||
|
|
||||||
'products.view',
|
'products.view',
|
||||||
|
'stocks.view',
|
||||||
'orders.view',
|
'orders.view',
|
||||||
|
'cuttings.view',
|
||||||
'cash.view',
|
'cash.view',
|
||||||
|
|
||||||
'expenses.view',
|
'expenses.view',
|
||||||
|
|
||||||
'activity_logs.view',
|
'activity_logs.view',
|
||||||
|
|
||||||
'employee_advances.view',
|
'employee_advances.view',
|
||||||
'employee_advances.create',
|
'employee_advances.create',
|
||||||
'employee_advances.update',
|
'employee_advances.update',
|
||||||
'employee_advances.delete',
|
'employee_advances.delete',
|
||||||
|
'employee_advances.pay',
|
||||||
'payroll.view',
|
'payroll.view',
|
||||||
], true);
|
], true);
|
||||||
})),
|
})),
|
||||||
@ -420,23 +405,19 @@ public function run(): void
|
|||||||
'non-operator' => array_values(array_filter($allPermissions, function ($p) {
|
'non-operator' => array_values(array_filter($allPermissions, function ($p) {
|
||||||
return in_array($p, [
|
return in_array($p, [
|
||||||
'dashboard.view',
|
'dashboard.view',
|
||||||
|
|
||||||
'analysis.view',
|
'analysis.view',
|
||||||
'analysis.attendance',
|
'analysis.attendance',
|
||||||
|
|
||||||
'attendances.view',
|
'attendances.view',
|
||||||
'attendances.create',
|
'attendances.create',
|
||||||
|
|
||||||
'leave_requests.view',
|
'leave_requests.view',
|
||||||
'leave_requests.create',
|
'leave_requests.create',
|
||||||
'leave_requests.update',
|
'leave_requests.update',
|
||||||
'leave_requests.delete',
|
'leave_requests.delete',
|
||||||
|
|
||||||
'employee_advances.view',
|
'employee_advances.view',
|
||||||
'employee_advances.create',
|
'employee_advances.create',
|
||||||
'employee_advances.update',
|
'employee_advances.update',
|
||||||
'employee_advances.delete',
|
'employee_advances.delete',
|
||||||
|
'employee_advances.pay',
|
||||||
'payroll.view',
|
'payroll.view',
|
||||||
], true);
|
], true);
|
||||||
})),
|
})),
|
||||||
@ -444,28 +425,24 @@ public function run(): void
|
|||||||
'stok-opname' => array_values(array_filter($allPermissions, function ($p) {
|
'stok-opname' => array_values(array_filter($allPermissions, function ($p) {
|
||||||
return in_array($p, [
|
return in_array($p, [
|
||||||
'dashboard.view',
|
'dashboard.view',
|
||||||
|
|
||||||
'attendances.view',
|
'attendances.view',
|
||||||
'attendances.create',
|
'attendances.create',
|
||||||
|
|
||||||
'leave_requests.view',
|
'leave_requests.view',
|
||||||
'leave_requests.create',
|
'leave_requests.create',
|
||||||
'leave_requests.update',
|
'leave_requests.update',
|
||||||
'leave_requests.delete',
|
'leave_requests.delete',
|
||||||
|
|
||||||
'products.view',
|
'products.view',
|
||||||
|
'stocks.view',
|
||||||
'stok_opnames.view',
|
'stok_opnames.view',
|
||||||
'stok_opnames.create',
|
'stok_opnames.create',
|
||||||
'stok_opnames.update',
|
'stok_opnames.update',
|
||||||
'stok_opnames.delete',
|
'stok_opnames.delete',
|
||||||
'stok_opnames.submit',
|
'stok_opnames.submit',
|
||||||
|
|
||||||
'employee_advances.view',
|
'employee_advances.view',
|
||||||
'employee_advances.create',
|
'employee_advances.create',
|
||||||
'employee_advances.update',
|
'employee_advances.update',
|
||||||
'employee_advances.delete',
|
'employee_advances.delete',
|
||||||
|
'employee_advances.pay',
|
||||||
'payroll.view',
|
'payroll.view',
|
||||||
], true);
|
], true);
|
||||||
})),
|
})),
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
|
import L from 'leaflet';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
|
||||||
interface LocationMapProps {
|
interface LocationMapProps {
|
||||||
latitude: number;
|
latitude: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
@ -9,18 +13,52 @@ export function LocationMap({
|
|||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
height = '250px',
|
height = '250px',
|
||||||
zoom = 17,
|
zoom = 15,
|
||||||
}: LocationMapProps) {
|
}: LocationMapProps) {
|
||||||
const src = `https://maps.google.com/maps?q=${latitude},${longitude}&z=${zoom}&t=k&output=embed`;
|
const mapRef = useRef<HTMLDivElement>(null);
|
||||||
|
const mapInstanceRef = useRef<L.Map | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mapRef.current || mapInstanceRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = L.map(mapRef.current, {
|
||||||
|
center: [latitude, longitude],
|
||||||
|
zoom,
|
||||||
|
zoomControl: false,
|
||||||
|
attributionControl: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
L.control.zoom({ position: 'topright' }).addTo(map);
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution:
|
||||||
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
const icon = L.divIcon({
|
||||||
|
html: `<div style="background: #ef4444; width: 24px; height: 24px; border-radius: 50%; border: 3px solid white; box-shadow: 0 2px 6px rgba(0,0,0,0.3);"></div>`,
|
||||||
|
className: '',
|
||||||
|
iconSize: [24, 24],
|
||||||
|
iconAnchor: [12, 12],
|
||||||
|
});
|
||||||
|
|
||||||
|
L.marker([latitude, longitude], { icon }).addTo(map);
|
||||||
|
|
||||||
|
mapInstanceRef.current = map;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
map.remove();
|
||||||
|
mapInstanceRef.current = null;
|
||||||
|
};
|
||||||
|
}, [latitude, longitude, zoom]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<iframe
|
<div
|
||||||
src={src}
|
ref={mapRef}
|
||||||
style={{ height, width: '100%', border: 0 }}
|
style={{ height, width: '100%' }}
|
||||||
className="rounded-lg"
|
className="rounded-lg"
|
||||||
loading="lazy"
|
|
||||||
referrerPolicy="no-referrer-when-downgrade"
|
|
||||||
title="Lokasi Presensi"
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export function createCashAccountColumns(
|
|||||||
): ColumnDef<CashAccount>[] {
|
): ColumnDef<CashAccount>[] {
|
||||||
const { handleEdit, handleDeleteClick, can } = params;
|
const { handleEdit, handleDeleteClick, can } = params;
|
||||||
|
|
||||||
const columns: ColumnDef<CashAccount>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: () => <span>Nama</span>,
|
header: () => <span>Nama</span>,
|
||||||
@ -39,10 +39,7 @@ export function createCashAccountColumns(
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (can('cash.update') || can('cash.delete')) {
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -73,8 +70,6 @@ export function createCashAccountColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
];
|
||||||
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { ImagePreviewButton } from '@/components/image-preview-button';
|
|
||||||
import { RowActions } from '@/components/row-actions';
|
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import { Pencil, Trash2 } from 'lucide-react';
|
import { Pencil, Trash2 } from 'lucide-react';
|
||||||
|
import { ImagePreviewButton } from '@/components/image-preview-button';
|
||||||
|
import { RowActions } from '@/components/row-actions';
|
||||||
|
|
||||||
export type CashTransaction = {
|
export type CashTransaction = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -30,12 +30,23 @@ function getTypeLabel(type: string): string {
|
|||||||
deposit: 'Deposit',
|
deposit: 'Deposit',
|
||||||
withdrawal: 'Withdrawal',
|
withdrawal: 'Withdrawal',
|
||||||
expense: 'Pengeluaran',
|
expense: 'Pengeluaran',
|
||||||
employee_advance: 'Kasbon',
|
transfer: 'Transfer',
|
||||||
};
|
};
|
||||||
|
|
||||||
return labels[type] ?? type;
|
return labels[type] ?? type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getReferenceLabel(type: string): string {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
'App\\Models\\Expense': 'Pengeluaran',
|
||||||
|
'App\\Models\\Order': 'Penjualan Tunai',
|
||||||
|
'App\\Models\\Purchase': 'Belanja',
|
||||||
|
'App\\Models\\CashAccount': 'Transfer Kas',
|
||||||
|
};
|
||||||
|
|
||||||
|
return labels[type] ?? '-';
|
||||||
|
}
|
||||||
|
|
||||||
type CreateColumnsParams = {
|
type CreateColumnsParams = {
|
||||||
handleEdit: (transaction: CashTransaction) => void;
|
handleEdit: (transaction: CashTransaction) => void;
|
||||||
handleDeleteClick: (transaction: CashTransaction) => void;
|
handleDeleteClick: (transaction: CashTransaction) => void;
|
||||||
@ -47,7 +58,7 @@ export function createTransactionColumns(
|
|||||||
): ColumnDef<CashTransaction>[] {
|
): ColumnDef<CashTransaction>[] {
|
||||||
const { handleEdit, handleDeleteClick, can } = params;
|
const { handleEdit, handleDeleteClick, can } = params;
|
||||||
|
|
||||||
const columns: ColumnDef<CashTransaction>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'formatted_created_at',
|
accessorKey: 'formatted_created_at',
|
||||||
header: () => <span>Tanggal</span>,
|
header: () => <span>Tanggal</span>,
|
||||||
@ -66,6 +77,11 @@ export function createTransactionColumns(
|
|||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{getTypeLabel(transaction.type)}
|
{getTypeLabel(transaction.type)}
|
||||||
</span>
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{getReferenceLabel(
|
||||||
|
transaction.reference?.type ?? '',
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -136,10 +152,7 @@ export function createTransactionColumns(
|
|||||||
return <span>{createdBy?.user_profile?.full_name ?? '-'}</span>;
|
return <span>{createdBy?.user_profile?.full_name ?? '-'}</span>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (can('cash.update') || can('cash.delete')) {
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -177,8 +190,6 @@ export function createTransactionColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
];
|
||||||
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export function createEmployeeAdvanceColumns(
|
|||||||
const { handleEdit, handleDeleteClick, handleApprove, handlePay, handleShowPayments, can, authUserId } =
|
const { handleEdit, handleDeleteClick, handleApprove, handlePay, handleShowPayments, can, authUserId } =
|
||||||
params;
|
params;
|
||||||
|
|
||||||
const columns: ColumnDef<EmployeeAdvance>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'formatted_created_at',
|
accessorKey: 'formatted_created_at',
|
||||||
header: () => <span>Tanggal</span>,
|
header: () => <span>Tanggal</span>,
|
||||||
@ -162,16 +162,7 @@ export function createEmployeeAdvanceColumns(
|
|||||||
<span>{getStatusBadge(row.getValue('status') as string)}</span>
|
<span>{getStatusBadge(row.getValue('status') as string)}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (
|
|
||||||
can('employee_advances.verify') ||
|
|
||||||
can('employee_advances.pay') ||
|
|
||||||
can('employee_advances.view_payments') ||
|
|
||||||
can('employee_advances.update') ||
|
|
||||||
can('employee_advances.delete')
|
|
||||||
) {
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -207,7 +198,7 @@ export function createEmployeeAdvanceColumns(
|
|||||||
{
|
{
|
||||||
label: 'Riwayat',
|
label: 'Riwayat',
|
||||||
icon: <History className="h-4 w-4" />,
|
icon: <History className="h-4 w-4" />,
|
||||||
show: can('employee_advances.view_payments') && employeeAdvance.payments.length > 0,
|
show: employeeAdvance.payments.length > 0,
|
||||||
onClick: () => handleShowPayments(employeeAdvance),
|
onClick: () => handleShowPayments(employeeAdvance),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -235,8 +226,6 @@ export function createEmployeeAdvanceColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
];
|
||||||
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export function createExpenseColumns(
|
|||||||
): ColumnDef<Expense>[] {
|
): ColumnDef<Expense>[] {
|
||||||
const { handleEdit, handleDeleteClick, can } = params;
|
const { handleEdit, handleDeleteClick, can } = params;
|
||||||
|
|
||||||
const columns: ColumnDef<Expense>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'formatted_created_at',
|
accessorKey: 'formatted_created_at',
|
||||||
header: () => <span>Tanggal</span>,
|
header: () => <span>Tanggal</span>,
|
||||||
@ -69,8 +69,8 @@ export function createExpenseColumns(
|
|||||||
accessorKey: 'formatted_amount',
|
accessorKey: 'formatted_amount',
|
||||||
header: () => <span>Jumlah</span>,
|
header: () => <span>Jumlah</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="font-medium">
|
<span className="font-medium text-red-600">
|
||||||
{row.getValue('formatted_amount') as string}
|
- {row.getValue('formatted_amount') as string}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -83,10 +83,7 @@ export function createExpenseColumns(
|
|||||||
return <span>{createdBy?.user_profile?.full_name ?? '-'}</span>;
|
return <span>{createdBy?.user_profile?.full_name ?? '-'}</span>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (can('expenses.update') || can('expenses.delete')) {
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -117,8 +114,6 @@ export function createExpenseColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
];
|
||||||
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -174,57 +174,58 @@ export function createPayrollPeriodColumns(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
columns.push({
|
columns.push(
|
||||||
accessorKey: 'status',
|
{
|
||||||
header: () => <span>Status</span>,
|
accessorKey: 'status',
|
||||||
cell: ({ row }) => (
|
header: () => <span>Status</span>,
|
||||||
<span>{getStatusBadge(row.getValue('status') as string)}</span>
|
cell: ({ row }) => (
|
||||||
),
|
<span>{getStatusBadge(row.getValue('status') as string)}</span>
|
||||||
});
|
),
|
||||||
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
|
||||||
meta: {
|
|
||||||
className: 'w-[100px] text-center',
|
|
||||||
headerClassName: 'w-[100px] text-center',
|
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
{
|
||||||
const period = row.original;
|
id: 'actions',
|
||||||
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
|
meta: {
|
||||||
|
className: 'w-[100px] text-center',
|
||||||
|
headerClassName: 'w-[100px] text-center',
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const period = row.original;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowActions
|
<RowActions
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
label: 'Lihat Detail',
|
label: 'Lihat Detail',
|
||||||
icon: <Eye className="h-4 w-4" />,
|
icon: <Eye className="h-4 w-4" />,
|
||||||
href: showUrl(period.id),
|
href: showUrl(period.id),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Tutup Periode',
|
label: 'Tutup Periode',
|
||||||
icon: (
|
icon: (
|
||||||
<Lock className="h-4 w-4 text-orange-600" />
|
<Lock className="h-4 w-4 text-orange-600" />
|
||||||
),
|
),
|
||||||
show:
|
show:
|
||||||
can('payroll.adjust') &&
|
can('payroll.adjust') &&
|
||||||
period.status === 'open',
|
period.status === 'open',
|
||||||
onClick: () => handleClose(period),
|
onClick: () => handleClose(period),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Buka Periode',
|
label: 'Buka Periode',
|
||||||
icon: (
|
icon: (
|
||||||
<Unlock className="h-4 w-4 text-blue-600" />
|
<Unlock className="h-4 w-4 text-blue-600" />
|
||||||
),
|
),
|
||||||
show:
|
show:
|
||||||
can('payroll.adjust') &&
|
can('payroll.adjust') &&
|
||||||
period.status === 'closed',
|
period.status === 'closed',
|
||||||
onClick: () => handleReopen(period),
|
onClick: () => handleReopen(period),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import { toast } from 'sonner';
|
|||||||
import { CameraCapture } from '@/components/camera-capture';
|
import { CameraCapture } from '@/components/camera-capture';
|
||||||
import { LocationMap } from '@/components/location-map';
|
import { LocationMap } from '@/components/location-map';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
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 {
|
import {
|
||||||
@ -45,7 +44,6 @@ type Attendance = {
|
|||||||
check_out_latitude: number | null;
|
check_out_latitude: number | null;
|
||||||
check_out_longitude: number | null;
|
check_out_longitude: number | null;
|
||||||
work_duration_minutes: number | null;
|
work_duration_minutes: number | null;
|
||||||
employee_name: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type MonthStats = {
|
type MonthStats = {
|
||||||
@ -60,12 +58,11 @@ type Props = {
|
|||||||
todayAttendance: Attendance | null;
|
todayAttendance: Attendance | null;
|
||||||
currentYear: number;
|
currentYear: number;
|
||||||
currentMonth: number;
|
currentMonth: number;
|
||||||
monthStats: MonthStats | null;
|
monthStats: MonthStats;
|
||||||
hrSettings: {
|
hrSettings: {
|
||||||
scheduled_check_in_time: string;
|
scheduled_check_in_time: string;
|
||||||
scheduled_check_out_time: string;
|
scheduled_check_out_time: string;
|
||||||
};
|
};
|
||||||
isAdmin: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const WEEKDAYS = ['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab'];
|
const WEEKDAYS = ['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab'];
|
||||||
@ -86,6 +83,16 @@ function isSameDay(d1: Date, d2: Date): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkIsToday(date: Date): boolean {
|
||||||
|
const t = new Date();
|
||||||
|
|
||||||
|
return (
|
||||||
|
date.getDate() === t.getDate() &&
|
||||||
|
date.getMonth() === t.getMonth() &&
|
||||||
|
date.getFullYear() === t.getFullYear()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isWeekend(date: Date): boolean {
|
function isWeekend(date: Date): boolean {
|
||||||
const day = date.getDay();
|
const day = date.getDay();
|
||||||
|
|
||||||
@ -98,8 +105,8 @@ function isLate(
|
|||||||
officeMinute: number,
|
officeMinute: number,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!checkInAt) {
|
if (!checkInAt) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const d = new Date(checkInAt);
|
const d = new Date(checkInAt);
|
||||||
const h = d.getHours();
|
const h = d.getHours();
|
||||||
@ -114,8 +121,8 @@ function getLateMinutes(
|
|||||||
officeMinute: number,
|
officeMinute: number,
|
||||||
): number {
|
): number {
|
||||||
if (!checkInAt || !isLate(checkInAt, officeHour, officeMinute)) {
|
if (!checkInAt || !isLate(checkInAt, officeHour, officeMinute)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const d = new Date(checkInAt);
|
const d = new Date(checkInAt);
|
||||||
const officeStart = new Date(d);
|
const officeStart = new Date(d);
|
||||||
@ -126,15 +133,15 @@ function getLateMinutes(
|
|||||||
|
|
||||||
function formatMinutes(minutes: number | null): string {
|
function formatMinutes(minutes: number | null): string {
|
||||||
if (!minutes) {
|
if (!minutes) {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
const mins = Math.floor(minutes % 60);
|
const mins = Math.floor(minutes % 60);
|
||||||
|
|
||||||
if (mins === 0) {
|
if (mins === 0) {
|
||||||
return `${hours} jam`;
|
return `${hours} jam`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${hours} jam ${mins} menit`;
|
return `${hours} jam ${mins} menit`;
|
||||||
}
|
}
|
||||||
@ -146,7 +153,6 @@ export default function AttendanceIndex({
|
|||||||
currentMonth,
|
currentMonth,
|
||||||
monthStats,
|
monthStats,
|
||||||
hrSettings,
|
hrSettings,
|
||||||
isAdmin,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [officeHour, officeMinute] = hrSettings.scheduled_check_in_time
|
const [officeHour, officeMinute] = hrSettings.scheduled_check_in_time
|
||||||
.split(':')
|
.split(':')
|
||||||
@ -167,17 +173,21 @@ export default function AttendanceIndex({
|
|||||||
const viewYear = viewDate.getFullYear();
|
const viewYear = viewDate.getFullYear();
|
||||||
const viewMonth = viewDate.getMonth() + 1;
|
const viewMonth = viewDate.getMonth() + 1;
|
||||||
|
|
||||||
const attendanceByDate = useMemo(() => {
|
const attendanceDates = useMemo(() => {
|
||||||
const map = new Map<string, Attendance[]>();
|
const dates = new Map<string, Attendance>();
|
||||||
attendances.forEach((att) => {
|
attendances.forEach((att) => {
|
||||||
const existing = map.get(att.attendance_date) ?? [];
|
dates.set(att.attendance_date, att);
|
||||||
existing.push(att);
|
|
||||||
map.set(att.attendance_date, existing);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return map;
|
return dates;
|
||||||
}, [attendances]);
|
}, [attendances]);
|
||||||
|
|
||||||
|
const selectedAttendance = useMemo(() => {
|
||||||
|
const dateStr = format(selectedDate, 'yyyy-MM-dd');
|
||||||
|
|
||||||
|
return attendanceDates.get(dateStr) ?? null;
|
||||||
|
}, [selectedDate, attendanceDates]);
|
||||||
|
|
||||||
const calendarDays = useMemo(() => {
|
const calendarDays = useMemo(() => {
|
||||||
const daysInMonth = getDaysInMonth(viewYear, viewMonth);
|
const daysInMonth = getDaysInMonth(viewYear, viewMonth);
|
||||||
const firstDay = getFirstDayOfMonth(viewYear, viewMonth);
|
const firstDay = getFirstDayOfMonth(viewYear, viewMonth);
|
||||||
@ -222,51 +232,8 @@ export default function AttendanceIndex({
|
|||||||
return days;
|
return days;
|
||||||
}, [viewYear, viewMonth]);
|
}, [viewYear, viewMonth]);
|
||||||
|
|
||||||
const handlePrevMonth = () => {
|
const handlePrevMonth = () => setViewDate((d) => subMonths(d, 1));
|
||||||
const newDate = subMonths(viewDate, 1);
|
const handleNextMonth = () => setViewDate((d) => addMonths(d, 1));
|
||||||
setViewDate(newDate);
|
|
||||||
|
|
||||||
if (!isAdmin) {
|
|
||||||
router.get(
|
|
||||||
attendanceIndex.url(),
|
|
||||||
{
|
|
||||||
year: newDate.getFullYear(),
|
|
||||||
month: newDate.getMonth() + 1,
|
|
||||||
},
|
|
||||||
{ preserveState: true, preserveScroll: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNextMonth = () => {
|
|
||||||
const newDate = addMonths(viewDate, 1);
|
|
||||||
setViewDate(newDate);
|
|
||||||
|
|
||||||
if (!isAdmin) {
|
|
||||||
router.get(
|
|
||||||
attendanceIndex.url(),
|
|
||||||
{
|
|
||||||
year: newDate.getFullYear(),
|
|
||||||
month: newDate.getMonth() + 1,
|
|
||||||
},
|
|
||||||
{ preserveState: true, preserveScroll: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGoToToday = () => {
|
|
||||||
const now = new Date();
|
|
||||||
setViewDate(now);
|
|
||||||
setSelectedDate(now);
|
|
||||||
|
|
||||||
if (!isAdmin) {
|
|
||||||
router.get(
|
|
||||||
attendanceIndex.url(),
|
|
||||||
{ year: now.getFullYear(), month: now.getMonth() + 1 },
|
|
||||||
{ preserveState: true, preserveScroll: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCameraCapture = (dataUrl: string) => {
|
const handleCameraCapture = (dataUrl: string) => {
|
||||||
setShowCamera(false);
|
setShowCamera(false);
|
||||||
@ -323,8 +290,8 @@ export default function AttendanceIndex({
|
|||||||
|
|
||||||
function formatTime(dateStr: string | null): string {
|
function formatTime(dateStr: string | null): string {
|
||||||
if (!dateStr) {
|
if (!dateStr) {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
return format(new Date(dateStr), 'HH:mm');
|
return format(new Date(dateStr), 'HH:mm');
|
||||||
}
|
}
|
||||||
@ -338,168 +305,167 @@ export default function AttendanceIndex({
|
|||||||
<>
|
<>
|
||||||
<Head title="Presensi" />
|
<Head title="Presensi" />
|
||||||
|
|
||||||
<div className="flex min-h-full flex-1 flex-col gap-6 overflow-auto p-4 md:p-6">
|
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-semibold tracking-tight">
|
<h2 className="text-2xl font-semibold tracking-tight">
|
||||||
Presensi
|
Presensi
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isAdmin && (
|
{/* Alert Status Presensi */}
|
||||||
<Alert>
|
<Alert>
|
||||||
<CalendarCheck className="h-4 w-4" />
|
<CalendarCheck className="h-4 w-4" />
|
||||||
<AlertTitle>Presensi Hari Ini</AlertTitle>
|
<AlertTitle>Presensi Hari Ini</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{hasCheckedIn ? (
|
{hasCheckedIn ? (
|
||||||
<div className="flex items-center gap-4 text-sm">
|
<div className="flex items-center gap-4 text-sm">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<CheckCircle2 className="h-3.5 w-3.5 text-green-600" />
|
<CheckCircle2 className="h-3.5 w-3.5 text-green-600" />
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
Masuk:
|
Masuk:
|
||||||
</span>
|
</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{formatTime(
|
{formatTime(
|
||||||
todayAttendance?.check_in_at,
|
todayAttendance?.check_in_at,
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
{hasCheckedOut ? (
|
|
||||||
<>
|
|
||||||
<CheckCircle2 className="h-3.5 w-3.5 text-green-600" />
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Pulang:
|
|
||||||
</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{formatTime(
|
|
||||||
todayAttendance?.check_out_at,
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Clock className="h-3.5 w-3.5 text-orange-500" />
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Pulang:
|
|
||||||
</span>
|
|
||||||
<span className="font-medium text-orange-600">
|
|
||||||
Belum
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<div className="flex items-center gap-1.5">
|
||||||
<p className="text-sm text-muted-foreground">
|
{hasCheckedOut ? (
|
||||||
Anda belum melakukan presensi hari ini
|
<>
|
||||||
</p>
|
<CheckCircle2 className="h-3.5 w-3.5 text-green-600" />
|
||||||
)}
|
<span className="text-muted-foreground">
|
||||||
</div>
|
Pulang:
|
||||||
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<span className="font-medium">
|
||||||
{locationLoading && (
|
{formatTime(
|
||||||
<span className="text-xs text-muted-foreground">
|
todayAttendance?.check_out_at,
|
||||||
Mendapatkan lokasi...
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
</>
|
||||||
<Button
|
) : (
|
||||||
size="sm"
|
<>
|
||||||
onClick={handleCheckIn}
|
<Clock className="h-3.5 w-3.5 text-orange-500" />
|
||||||
disabled={hasCheckedIn || locationLoading}
|
<span className="text-muted-foreground">
|
||||||
>
|
Pulang:
|
||||||
<LogIn className="mr-1.5 h-3.5 w-3.5" />
|
</span>
|
||||||
Presensi Masuk
|
<span className="font-medium text-orange-600">
|
||||||
</Button>
|
Belum
|
||||||
<Button
|
</span>
|
||||||
size="sm"
|
</>
|
||||||
variant="outline"
|
)}
|
||||||
onClick={handleCheckOut}
|
</div>
|
||||||
disabled={
|
</div>
|
||||||
!hasCheckedIn ||
|
) : (
|
||||||
hasCheckedOut ||
|
<p className="text-sm text-muted-foreground">
|
||||||
locationLoading
|
Anda belum melakukan presensi hari ini
|
||||||
}
|
</p>
|
||||||
>
|
)}
|
||||||
<LogOut className="mr-1.5 h-3.5 w-3.5" />
|
|
||||||
Presensi Pulang
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{monthStats && (
|
<div className="flex items-center gap-2">
|
||||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
{locationLoading && (
|
||||||
<Card>
|
<span className="text-xs text-muted-foreground">
|
||||||
<CardContent className="flex items-center gap-3 py-3">
|
Mendapatkan lokasi...
|
||||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-blue-100">
|
</span>
|
||||||
<CalendarDays className="h-5 w-5 text-blue-600" />
|
)}
|
||||||
</div>
|
<Button
|
||||||
<div>
|
size="sm"
|
||||||
<p className="text-xs text-muted-foreground">
|
onClick={handleCheckIn}
|
||||||
Hari Kerja
|
disabled={hasCheckedIn || locationLoading}
|
||||||
</p>
|
>
|
||||||
<p className="text-lg font-bold">
|
<LogIn className="mr-1.5 h-3.5 w-3.5" />
|
||||||
{monthStats.working_days}
|
Presensi Masuk
|
||||||
</p>
|
</Button>
|
||||||
</div>
|
<Button
|
||||||
</CardContent>
|
size="sm"
|
||||||
</Card>
|
variant="outline"
|
||||||
<Card>
|
onClick={handleCheckOut}
|
||||||
<CardContent className="flex items-center gap-3 py-3">
|
disabled={
|
||||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-green-100">
|
!hasCheckedIn ||
|
||||||
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
hasCheckedOut ||
|
||||||
</div>
|
locationLoading
|
||||||
<div>
|
}
|
||||||
<p className="text-xs text-muted-foreground">
|
>
|
||||||
Hadir
|
<LogOut className="mr-1.5 h-3.5 w-3.5" />
|
||||||
</p>
|
Presensi Pulang
|
||||||
<p className="text-lg font-bold">
|
</Button>
|
||||||
{monthStats.present}
|
</div>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</AlertDescription>
|
||||||
</CardContent>
|
</Alert>
|
||||||
</Card>
|
|
||||||
<Card>
|
{/* Summary Stats */}
|
||||||
<CardContent className="flex items-center gap-3 py-3">
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-red-100">
|
<Card>
|
||||||
<UserX className="h-5 w-5 text-red-600" />
|
<CardContent className="flex items-center gap-3 py-3">
|
||||||
</div>
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-blue-100">
|
||||||
<div>
|
<CalendarDays className="h-5 w-5 text-blue-600" />
|
||||||
<p className="text-xs text-muted-foreground">
|
</div>
|
||||||
Tidak Hadir
|
<div>
|
||||||
</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
<p className="text-lg font-bold">
|
Hari Kerja
|
||||||
{monthStats.absent}
|
</p>
|
||||||
</p>
|
<p className="text-lg font-bold">
|
||||||
</div>
|
{monthStats.working_days}
|
||||||
</CardContent>
|
</p>
|
||||||
</Card>
|
</div>
|
||||||
<Card>
|
</CardContent>
|
||||||
<CardContent className="flex items-center gap-3 py-3">
|
</Card>
|
||||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-amber-100">
|
<Card>
|
||||||
<Wallet className="h-5 w-5 text-amber-600" />
|
<CardContent className="flex items-center gap-3 py-3">
|
||||||
</div>
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-green-100">
|
||||||
<div>
|
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
||||||
<p className="text-xs text-muted-foreground">
|
</div>
|
||||||
Cuti
|
<div>
|
||||||
</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
<p className="text-lg font-bold">
|
Hadir
|
||||||
{monthStats.leave}
|
</p>
|
||||||
</p>
|
<p className="text-lg font-bold">
|
||||||
</div>
|
{monthStats.present}
|
||||||
</CardContent>
|
</p>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
)}
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex items-center gap-3 py-3">
|
||||||
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-red-100">
|
||||||
|
<UserX className="h-5 w-5 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Tidak Hadir
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-bold">
|
||||||
|
{monthStats.absent}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex items-center gap-3 py-3">
|
||||||
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-amber-100">
|
||||||
|
<Wallet className="h-5 w-5 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Cuti
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-bold">
|
||||||
|
{monthStats.leave}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
style={{ '--card-spacing': '0px' } as React.CSSProperties}
|
style={{ '--card-spacing': '0px' } as React.CSSProperties}
|
||||||
>
|
>
|
||||||
|
{/* Calendar Header */}
|
||||||
<div className="flex items-center justify-between border-b px-6 py-4">
|
<div className="flex items-center justify-between border-b px-6 py-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex flex-col items-center overflow-hidden rounded-lg ring-1 ring-border">
|
<div className="flex flex-col items-center overflow-hidden rounded-lg ring-1 ring-border">
|
||||||
@ -537,7 +503,10 @@ export default function AttendanceIndex({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 px-3 text-xs font-medium"
|
className="h-8 px-3 text-xs font-medium"
|
||||||
onClick={handleGoToToday}
|
onClick={() => {
|
||||||
|
setViewDate(new Date());
|
||||||
|
setSelectedDate(new Date());
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Hari ini
|
Hari ini
|
||||||
</Button>
|
</Button>
|
||||||
@ -552,6 +521,7 @@ export default function AttendanceIndex({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Weekday Headers */}
|
||||||
<div className="grid grid-cols-7 border-b">
|
<div className="grid grid-cols-7 border-b">
|
||||||
{WEEKDAYS.map((day) => (
|
{WEEKDAYS.map((day) => (
|
||||||
<div
|
<div
|
||||||
@ -563,10 +533,11 @@ export default function AttendanceIndex({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar Grid */}
|
||||||
<div className="grid grid-cols-7">
|
<div className="grid grid-cols-7">
|
||||||
{calendarDays.map((cell, idx) => {
|
{calendarDays.map((cell, idx) => {
|
||||||
const dateStr = format(cell.date, 'yyyy-MM-dd');
|
const dateStr = format(cell.date, 'yyyy-MM-dd');
|
||||||
const dayAttendances = attendanceByDate.get(dateStr) ?? [];
|
const attendance = attendanceDates.get(dateStr);
|
||||||
const isSelected = isSameDay(
|
const isSelected = isSameDay(
|
||||||
cell.date,
|
cell.date,
|
||||||
selectedDate,
|
selectedDate,
|
||||||
@ -576,33 +547,47 @@ export default function AttendanceIndex({
|
|||||||
new Date(),
|
new Date(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const todayMidnight = new Date();
|
const today = new Date();
|
||||||
todayMidnight.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
const cellDate = new Date(cell.date);
|
const cellDate = new Date(cell.date);
|
||||||
cellDate.setHours(0, 0, 0, 0);
|
cellDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
const isPastDate = cellDate < todayMidnight;
|
const isPastDate = cellDate < today;
|
||||||
const showAbsent =
|
const showAbsent =
|
||||||
cell.isCurrentMonth &&
|
cell.isCurrentMonth &&
|
||||||
isPastDate &&
|
isPastDate &&
|
||||||
!isWeekend(cell.date) &&
|
!isWeekend(cell.date) &&
|
||||||
dayAttendances.length === 0;
|
!attendance;
|
||||||
|
|
||||||
|
const late = attendance
|
||||||
|
? isLate(
|
||||||
|
attendance.check_in_at,
|
||||||
|
officeHour,
|
||||||
|
officeMinute,
|
||||||
|
)
|
||||||
|
: false;
|
||||||
|
const lateMins = attendance
|
||||||
|
? getLateMinutes(
|
||||||
|
attendance.check_in_at,
|
||||||
|
officeHour,
|
||||||
|
officeMinute,
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedDate(cell.date);
|
setSelectedDate(cell.date);
|
||||||
if (!isAdmin && dayAttendances.length > 0) {
|
|
||||||
setDetailAttendance(dayAttendances[0]);
|
if (attendance) {
|
||||||
}
|
setDetailAttendance(attendance);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className={`group relative flex min-h-[100px] flex-col p-2 text-left transition-colors hover:bg-muted/50 ${
|
className={`group relative flex min-h-[100px] flex-col p-2 text-left transition-colors hover:bg-muted/50 ${
|
||||||
!cell.isCurrentMonth
|
!cell.isCurrentMonth
|
||||||
? 'bg-muted/30 text-muted-foreground/50'
|
? 'bg-muted/30 text-muted-foreground/50'
|
||||||
: ''
|
: ''
|
||||||
} ${isSelected ? 'bg-muted/50' : ''} ${
|
|
||||||
!isAdmin && dayAttendances.length > 0 ? 'cursor-pointer' : ''
|
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
borderRight: '1px solid var(--border)',
|
borderRight: '1px solid var(--border)',
|
||||||
@ -622,89 +607,43 @@ export default function AttendanceIndex({
|
|||||||
{cell.day}
|
{cell.day}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex flex-col gap-0.5 overflow-hidden">
|
<div className="mt-1 flex flex-col gap-0.5">
|
||||||
{isAdmin ? (
|
{attendance && (
|
||||||
<>
|
<>
|
||||||
{dayAttendances.map((att) => {
|
<span
|
||||||
const late = isLate(
|
className={`inline-flex items-center justify-center rounded-full px-1.5 py-0.5 text-[10px] font-semibold ${late ? 'bg-yellow-100 text-yellow-700' : 'bg-green-100 text-green-700'}`}
|
||||||
att.check_in_at,
|
>
|
||||||
officeHour,
|
{late
|
||||||
officeMinute,
|
? 'Terlambat'
|
||||||
);
|
: 'Hadir'}
|
||||||
|
</span>
|
||||||
return (
|
<span className="text-[10px] text-muted-foreground">
|
||||||
<button
|
Masuk :{' '}
|
||||||
key={att.id}
|
{formatTime(
|
||||||
onClick={(e) => {
|
attendance.check_in_at,
|
||||||
e.stopPropagation();
|
)}
|
||||||
setDetailAttendance(att);
|
</span>
|
||||||
}}
|
<span className="text-[10px] text-muted-foreground">
|
||||||
className="w-full"
|
Pulang :{' '}
|
||||||
>
|
{formatTime(
|
||||||
<Badge
|
attendance.check_out_at,
|
||||||
variant={late ? 'destructive' : 'default'}
|
)}
|
||||||
className="w-full justify-center cursor-pointer truncate"
|
</span>
|
||||||
>
|
{late && (
|
||||||
{att.employee_name}
|
<span className="text-[10px] text-yellow-600">
|
||||||
</Badge>
|
Telat :{' '}
|
||||||
</button>
|
{formatMinutes(
|
||||||
);
|
lateMins,
|
||||||
})}
|
)}{' '}
|
||||||
</>
|
menit
|
||||||
) : (
|
</span>
|
||||||
<>
|
)}
|
||||||
{dayAttendances.length > 0 && (() => {
|
<span className="text-[10px] text-muted-foreground">
|
||||||
const att = dayAttendances[0];
|
Jam Kerja :{' '}
|
||||||
const late = isLate(
|
{formatMinutes(
|
||||||
att.check_in_at,
|
attendance.work_duration_minutes,
|
||||||
officeHour,
|
)}
|
||||||
officeMinute,
|
</span>
|
||||||
);
|
|
||||||
const lateMins = getLateMinutes(
|
|
||||||
att.check_in_at,
|
|
||||||
officeHour,
|
|
||||||
officeMinute,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center justify-center rounded-full px-1.5 py-0.5 text-[10px] font-semibold ${late ? 'bg-yellow-100 text-yellow-700' : 'bg-green-100 text-green-700'}`}
|
|
||||||
>
|
|
||||||
{late
|
|
||||||
? 'Terlambat'
|
|
||||||
: 'Hadir'}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-muted-foreground">
|
|
||||||
Masuk :{' '}
|
|
||||||
{formatTime(
|
|
||||||
att.check_in_at,
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-muted-foreground">
|
|
||||||
Pulang :{' '}
|
|
||||||
{formatTime(
|
|
||||||
att.check_out_at,
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
{late && (
|
|
||||||
<span className="text-[10px] text-yellow-600">
|
|
||||||
Telat :{' '}
|
|
||||||
{formatMinutes(
|
|
||||||
lateMins,
|
|
||||||
)}{' '}
|
|
||||||
menit
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="text-[10px] text-muted-foreground">
|
|
||||||
Jam Kerja :{' '}
|
|
||||||
{formatMinutes(
|
|
||||||
att.work_duration_minutes,
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{showAbsent && (
|
{showAbsent && (
|
||||||
@ -713,7 +652,7 @@ export default function AttendanceIndex({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@ -734,9 +673,6 @@ export default function AttendanceIndex({
|
|||||||
<DialogContent className="sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{isAdmin && detailAttendance?.employee_name
|
|
||||||
? `${detailAttendance.employee_name} - `
|
|
||||||
: ''}
|
|
||||||
Detail Presensi -{' '}
|
Detail Presensi -{' '}
|
||||||
{detailAttendance &&
|
{detailAttendance &&
|
||||||
format(
|
format(
|
||||||
@ -813,37 +749,19 @@ export default function AttendanceIndex({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-col gap-2">
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
Lokasi Presensi
|
||||||
Lokasi Masuk
|
</span>
|
||||||
</span>
|
<LocationMap
|
||||||
<LocationMap
|
latitude={
|
||||||
latitude={
|
detailAttendance.check_in_latitude
|
||||||
detailAttendance.check_in_latitude
|
}
|
||||||
}
|
longitude={
|
||||||
longitude={
|
detailAttendance.check_in_longitude
|
||||||
detailAttendance.check_in_longitude
|
}
|
||||||
}
|
height="200px"
|
||||||
height="200px"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{detailAttendance.check_out_latitude && detailAttendance.check_out_longitude && (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
|
||||||
Lokasi Pulang
|
|
||||||
</span>
|
|
||||||
<LocationMap
|
|
||||||
latitude={
|
|
||||||
detailAttendance.check_out_latitude
|
|
||||||
}
|
|
||||||
longitude={
|
|
||||||
detailAttendance.check_out_longitude
|
|
||||||
}
|
|
||||||
height="200px"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export function createEmployeeColumns(
|
|||||||
canViewAll,
|
canViewAll,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
const columns: ColumnDef<Employee>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'user_profile.full_name',
|
accessorKey: 'user_profile.full_name',
|
||||||
id: 'full_name',
|
id: 'full_name',
|
||||||
@ -154,14 +154,7 @@ export function createEmployeeColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (
|
|
||||||
can('employees.update') ||
|
|
||||||
can('employees.reset_password') ||
|
|
||||||
can('employees.delete')
|
|
||||||
) {
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -200,8 +193,6 @@ export function createEmployeeColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
];
|
||||||
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -161,41 +161,35 @@ export default function EmployeeEdit({ employee, roles, canViewAll }: Props) {
|
|||||||
*
|
*
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
{canViewAll ? (
|
<Combobox
|
||||||
<Combobox
|
items={roles}
|
||||||
items={roles}
|
itemToStringLabel={(r) => r.name}
|
||||||
itemToStringLabel={(r) => r.name}
|
value={selectedRole}
|
||||||
value={selectedRole}
|
onValueChange={(value) =>
|
||||||
onValueChange={(value) =>
|
setSelectedRole(value)
|
||||||
setSelectedRole(value)
|
}
|
||||||
}
|
>
|
||||||
>
|
<ComboboxInput
|
||||||
<ComboboxInput
|
placeholder="Cari role..."
|
||||||
placeholder="Cari role..."
|
className="w-full"
|
||||||
className="w-full"
|
/>
|
||||||
/>
|
<ComboboxContent>
|
||||||
<ComboboxContent>
|
<ComboboxEmpty>
|
||||||
<ComboboxEmpty>
|
Tidak ada role
|
||||||
Tidak ada role
|
ditemukan.
|
||||||
ditemukan.
|
</ComboboxEmpty>
|
||||||
</ComboboxEmpty>
|
<ComboboxList>
|
||||||
<ComboboxList>
|
{(role) => (
|
||||||
{(role) => (
|
<ComboboxItem
|
||||||
<ComboboxItem
|
key={role.id}
|
||||||
key={role.id}
|
value={role}
|
||||||
value={role}
|
>
|
||||||
>
|
{role.name}
|
||||||
{role.name}
|
</ComboboxItem>
|
||||||
</ComboboxItem>
|
)}
|
||||||
)}
|
</ComboboxList>
|
||||||
</ComboboxList>
|
</ComboboxContent>
|
||||||
</ComboboxContent>
|
</Combobox>
|
||||||
</Combobox>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-10 w-full items-center rounded-md border border-input bg-muted px-3 py-2 text-sm">
|
|
||||||
{selectedRole?.name ?? '-'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<InputError
|
<InputError
|
||||||
message={errors.role}
|
message={errors.role}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export function createLeaveRequestColumns(
|
|||||||
const { handleEdit, handleDeleteClick, handleApprove, handleReject, can } =
|
const { handleEdit, handleDeleteClick, handleApprove, handleReject, can } =
|
||||||
params;
|
params;
|
||||||
|
|
||||||
const columns: ColumnDef<LeaveRequest>[] = [
|
return [
|
||||||
{
|
{
|
||||||
id: 'employee_name',
|
id: 'employee_name',
|
||||||
header: () => <span>Oleh</span>,
|
header: () => <span>Oleh</span>,
|
||||||
@ -112,14 +112,7 @@ export function createLeaveRequestColumns(
|
|||||||
<span>{getStatusBadge(row.getValue('status') as string)}</span>
|
<span>{getStatusBadge(row.getValue('status') as string)}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (
|
|
||||||
can('leave_requests.verify') ||
|
|
||||||
can('leave_requests.update') ||
|
|
||||||
can('leave_requests.delete')
|
|
||||||
) {
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -174,8 +167,6 @@ export function createLeaveRequestColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
];
|
||||||
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,27 +139,25 @@ export function CuttingCardRow({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(can('cuttings.update') || can('cuttings.delete')) && (
|
<RowActions
|
||||||
<RowActions
|
actions={[
|
||||||
actions={[
|
{
|
||||||
{
|
label: 'Edit',
|
||||||
label: 'Edit',
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
show: can('cuttings.update'),
|
||||||
show: can('cuttings.update'),
|
onClick: () => onEdit(cutting),
|
||||||
onClick: () => onEdit(cutting),
|
},
|
||||||
},
|
{
|
||||||
{
|
label: 'Hapus',
|
||||||
label: 'Hapus',
|
icon: (
|
||||||
icon: (
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
),
|
||||||
),
|
show: can('cuttings.delete'),
|
||||||
show: can('cuttings.delete'),
|
onClick: () => onDelete(cutting),
|
||||||
onClick: () => onDelete(cutting),
|
},
|
||||||
},
|
]}
|
||||||
]}
|
wrapperClassName="flex shrink-0 items-center gap-1"
|
||||||
wrapperClassName="flex shrink-0 items-center gap-1"
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -126,27 +126,25 @@ export function PurchaseCardRow({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(can('purchases.update') || can('purchases.delete')) && (
|
<RowActions
|
||||||
<RowActions
|
actions={[
|
||||||
actions={[
|
{
|
||||||
{
|
label: 'Edit',
|
||||||
label: 'Edit',
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
show: can('purchases.update'),
|
||||||
show: can('purchases.update'),
|
onClick: () => onEdit(purchase),
|
||||||
onClick: () => onEdit(purchase),
|
},
|
||||||
},
|
{
|
||||||
{
|
label: 'Hapus',
|
||||||
label: 'Hapus',
|
icon: (
|
||||||
icon: (
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
),
|
||||||
),
|
show: can('purchases.delete'),
|
||||||
show: can('purchases.delete'),
|
onClick: () => onDelete(purchase),
|
||||||
onClick: () => onDelete(purchase),
|
},
|
||||||
},
|
]}
|
||||||
]}
|
wrapperClassName="flex shrink-0 items-center gap-1"
|
||||||
wrapperClassName="flex shrink-0 items-center gap-1"
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -135,27 +135,25 @@ export function RestockCardRow({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(can('restocks.update') || can('restocks.delete')) && (
|
<RowActions
|
||||||
<RowActions
|
actions={[
|
||||||
actions={[
|
{
|
||||||
{
|
label: 'Edit',
|
||||||
label: 'Edit',
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
show: can('restocks.update'),
|
||||||
show: can('restocks.update'),
|
onClick: () => onEdit(restock),
|
||||||
onClick: () => onEdit(restock),
|
},
|
||||||
},
|
{
|
||||||
{
|
label: 'Hapus',
|
||||||
label: 'Hapus',
|
icon: (
|
||||||
icon: (
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
),
|
||||||
),
|
show: can('restocks.delete'),
|
||||||
show: can('restocks.delete'),
|
onClick: () => onDelete(restock),
|
||||||
onClick: () => onDelete(restock),
|
},
|
||||||
},
|
]}
|
||||||
]}
|
wrapperClassName="flex shrink-0 items-center gap-1"
|
||||||
wrapperClassName="flex shrink-0 items-center gap-1"
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -183,59 +183,57 @@ export function TransactionCardRow({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(can('orders.update') || can('orders.delete')) && (
|
<RowActions
|
||||||
<RowActions
|
actions={[
|
||||||
actions={[
|
...(transaction.status === 'pending' && can('orders.update')
|
||||||
...(transaction.status === 'pending' && can('orders.update')
|
? [
|
||||||
? [
|
{
|
||||||
{
|
label: 'Kirim',
|
||||||
label: 'Kirim',
|
icon: <Send className="h-4 w-4" />,
|
||||||
icon: <Send className="h-4 w-4" />,
|
onClick: () => onUpdateStatus(transaction, 'processing'),
|
||||||
onClick: () => onUpdateStatus(transaction, 'processing'),
|
},
|
||||||
},
|
]
|
||||||
]
|
: []),
|
||||||
: []),
|
...((transaction.status === 'pending' || transaction.status === 'processing') && can('orders.update')
|
||||||
...((transaction.status === 'pending' || transaction.status === 'processing') && can('orders.update')
|
? [
|
||||||
? [
|
{
|
||||||
{
|
label: 'Selesai',
|
||||||
label: 'Selesai',
|
icon: <CheckCircle className="h-4 w-4" />,
|
||||||
icon: <CheckCircle className="h-4 w-4" />,
|
onClick: () => onUpdateStatus(transaction, 'completed'),
|
||||||
onClick: () => onUpdateStatus(transaction, 'completed'),
|
},
|
||||||
},
|
{
|
||||||
{
|
label: 'Dibatalkan',
|
||||||
label: 'Dibatalkan',
|
icon: <XCircle className="h-4 w-4 text-destructive" />,
|
||||||
icon: <XCircle className="h-4 w-4 text-destructive" />,
|
onClick: () => onUpdateStatus(transaction, 'cancelled'),
|
||||||
onClick: () => onUpdateStatus(transaction, 'cancelled'),
|
},
|
||||||
},
|
]
|
||||||
]
|
: []),
|
||||||
: []),
|
...(transaction.status === 'completed' && can('orders.update')
|
||||||
...(transaction.status === 'completed' && can('orders.update')
|
? [
|
||||||
? [
|
{
|
||||||
{
|
label: 'Refund',
|
||||||
label: 'Refund',
|
icon: <XCircle className="h-4 w-4 text-destructive" />,
|
||||||
icon: <XCircle className="h-4 w-4 text-destructive" />,
|
onClick: () => onUpdateStatus(transaction, 'refunded'),
|
||||||
onClick: () => onUpdateStatus(transaction, 'refunded'),
|
},
|
||||||
},
|
]
|
||||||
]
|
: []),
|
||||||
: []),
|
{
|
||||||
{
|
label: 'Edit',
|
||||||
label: 'Edit',
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
show: can('orders.update'),
|
||||||
show: can('orders.update'),
|
onClick: () => onEdit(transaction),
|
||||||
onClick: () => onEdit(transaction),
|
},
|
||||||
},
|
{
|
||||||
{
|
label: 'Hapus',
|
||||||
label: 'Hapus',
|
icon: (
|
||||||
icon: (
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
),
|
||||||
),
|
show: can('orders.delete'),
|
||||||
show: can('orders.delete'),
|
onClick: () => onDelete(transaction),
|
||||||
onClick: () => onDelete(transaction),
|
},
|
||||||
},
|
]}
|
||||||
]}
|
wrapperClassName="flex shrink-0 items-center gap-1"
|
||||||
wrapperClassName="flex shrink-0 items-center gap-1"
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export function createCategoryColumns(
|
|||||||
): ColumnDef<Category>[] {
|
): ColumnDef<Category>[] {
|
||||||
const { handleEdit, handleDeleteClick, can } = params;
|
const { handleEdit, handleDeleteClick, can } = params;
|
||||||
|
|
||||||
const columns: ColumnDef<Category>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: () => <span>Nama</span>,
|
header: () => <span>Nama</span>,
|
||||||
@ -28,10 +28,7 @@ export function createCategoryColumns(
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (can('categories.update') || can('categories.delete')) {
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -62,8 +59,6 @@ export function createCategoryColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
];
|
||||||
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export function createCustomerColumns(
|
|||||||
): ColumnDef<Customer>[] {
|
): ColumnDef<Customer>[] {
|
||||||
const { handleEdit, handleDeleteClick, can } = params;
|
const { handleEdit, handleDeleteClick, can } = params;
|
||||||
|
|
||||||
const columns: ColumnDef<Customer>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: () => <span>Nama</span>,
|
header: () => <span>Nama</span>,
|
||||||
@ -46,10 +46,7 @@ export function createCustomerColumns(
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (can('customers.update') || can('customers.delete')) {
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -80,8 +77,6 @@ export function createCustomerColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
];
|
||||||
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,7 +90,7 @@ export function createProductColumns(
|
|||||||
can,
|
can,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
const columns: ColumnDef<Product>[] = [
|
return [
|
||||||
{
|
{
|
||||||
id: 'expand',
|
id: 'expand',
|
||||||
header: '',
|
header: '',
|
||||||
@ -338,10 +338,7 @@ export function createProductColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (can('products.update') || can('products.delete')) {
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -372,8 +369,6 @@ export function createProductColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
];
|
||||||
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -144,27 +144,25 @@ export function ProductCardRow({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(can('products.update') || can('products.delete')) && (
|
<RowActions
|
||||||
<RowActions
|
actions={[
|
||||||
actions={[
|
{
|
||||||
{
|
label: 'Edit',
|
||||||
label: 'Edit',
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
show: can('products.update'),
|
||||||
show: can('products.update'),
|
onClick: () => onEdit(product),
|
||||||
onClick: () => onEdit(product),
|
},
|
||||||
},
|
{
|
||||||
{
|
label: 'Hapus',
|
||||||
label: 'Hapus',
|
icon: (
|
||||||
icon: (
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
),
|
||||||
),
|
show: can('products.delete'),
|
||||||
show: can('products.delete'),
|
onClick: () => onDelete(product),
|
||||||
onClick: () => onDelete(product),
|
},
|
||||||
},
|
]}
|
||||||
]}
|
wrapperClassName="flex shrink-0 items-center gap-1"
|
||||||
wrapperClassName="flex shrink-0 items-center gap-1"
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -34,8 +34,6 @@ export function VariantSubRow({
|
|||||||
variant: ProductVariant;
|
variant: ProductVariant;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const canAnyAction = can('products.transfer_stock') || can('products.view_stock_mutations') || can('products.update') || can('products.delete');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
@ -54,18 +52,16 @@ export function VariantSubRow({
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-center">Stok Ecer</TableHead>
|
<TableHead className="text-center">Stok Ecer</TableHead>
|
||||||
<TableHead>Harga</TableHead>
|
<TableHead>Harga</TableHead>
|
||||||
{canAnyAction && (
|
<TableHead className="w-[120px] text-center">
|
||||||
<TableHead className="w-[120px] text-center">
|
Aksi
|
||||||
Aksi
|
</TableHead>
|
||||||
</TableHead>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{variants.length === 0 ? (
|
{variants.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={canAnyAction ? 8 : 7}
|
colSpan={8}
|
||||||
className="text-center text-muted-foreground"
|
className="text-center text-muted-foreground"
|
||||||
>
|
>
|
||||||
Tidak ada varian.
|
Tidak ada varian.
|
||||||
@ -120,65 +116,63 @@ export function VariantSubRow({
|
|||||||
'-'
|
'-'
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{canAnyAction && (
|
<TableCell>
|
||||||
<TableCell>
|
<RowActions
|
||||||
<RowActions
|
actions={[
|
||||||
actions={[
|
{
|
||||||
{
|
label: 'Transfer Stok',
|
||||||
label: 'Transfer Stok',
|
icon: (
|
||||||
icon: (
|
<ArrowRightLeft className="h-4 w-4" />
|
||||||
<ArrowRightLeft className="h-4 w-4" />
|
),
|
||||||
),
|
show: can('stocks.view'),
|
||||||
show: can('products.transfer_stock'),
|
onClick: () =>
|
||||||
onClick: () =>
|
setTransferVariant({
|
||||||
setTransferVariant({
|
product,
|
||||||
product,
|
variant,
|
||||||
variant,
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mutasi Stok',
|
||||||
|
icon: (
|
||||||
|
<ScrollText className="h-4 w-4" />
|
||||||
|
),
|
||||||
|
show: can('stocks.view'),
|
||||||
|
onClick: () => {
|
||||||
|
router.visit(
|
||||||
|
stockMutations.url({
|
||||||
|
product: product.id,
|
||||||
|
variant: variant.id,
|
||||||
}),
|
}),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
label: 'Mutasi Stok',
|
{
|
||||||
icon: (
|
label: 'Edit',
|
||||||
<ScrollText className="h-4 w-4" />
|
icon: (
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
),
|
||||||
|
show: can('products.update'),
|
||||||
|
onClick: () =>
|
||||||
|
onEditVariant(
|
||||||
|
product,
|
||||||
|
variant,
|
||||||
),
|
),
|
||||||
show: can('products.view_stock_mutations'),
|
},
|
||||||
onClick: () => {
|
{
|
||||||
router.visit(
|
label: 'Hapus',
|
||||||
stockMutations.url({
|
icon: (
|
||||||
product: product.id,
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
variant: variant.id,
|
),
|
||||||
}),
|
show: can('products.delete'),
|
||||||
);
|
onClick: () =>
|
||||||
},
|
onDeleteVariantClick(
|
||||||
},
|
product,
|
||||||
{
|
variant,
|
||||||
label: 'Edit',
|
|
||||||
icon: (
|
|
||||||
<Pencil className="h-4 w-4" />
|
|
||||||
),
|
),
|
||||||
show: can('products.update'),
|
},
|
||||||
onClick: () =>
|
]}
|
||||||
onEditVariant(
|
/>
|
||||||
product,
|
</TableCell>
|
||||||
variant,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Hapus',
|
|
||||||
icon: (
|
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
|
||||||
),
|
|
||||||
show: can('products.delete'),
|
|
||||||
onClick: () =>
|
|
||||||
onDeleteVariantClick(
|
|
||||||
product,
|
|
||||||
variant,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -94,27 +94,25 @@ export function RawMaterialCardRow({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(can('raw_materials.update') || can('raw_materials.delete')) && (
|
<RowActions
|
||||||
<RowActions
|
actions={[
|
||||||
actions={[
|
{
|
||||||
{
|
label: 'Edit',
|
||||||
label: 'Edit',
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
show: can('raw_materials.update'),
|
||||||
show: can('raw_materials.update'),
|
onClick: () => onEdit(rawMaterial),
|
||||||
onClick: () => onEdit(rawMaterial),
|
},
|
||||||
},
|
{
|
||||||
{
|
label: 'Hapus',
|
||||||
label: 'Hapus',
|
icon: (
|
||||||
icon: (
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
),
|
||||||
),
|
show: can('raw_materials.delete'),
|
||||||
show: can('raw_materials.delete'),
|
onClick: () => onDelete(rawMaterial),
|
||||||
onClick: () => onDelete(rawMaterial),
|
},
|
||||||
},
|
]}
|
||||||
]}
|
wrapperClassName="flex shrink-0 items-center gap-1"
|
||||||
wrapperClassName="flex shrink-0 items-center gap-1"
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -47,8 +47,6 @@ export function RawMaterialVariantSubRow({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const canAnyAction = can('raw_materials.update') || can('raw_materials.delete');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
@ -61,18 +59,16 @@ export function RawMaterialVariantSubRow({
|
|||||||
<TableHead>Nama Varian</TableHead>
|
<TableHead>Nama Varian</TableHead>
|
||||||
<TableHead>Harga</TableHead>
|
<TableHead>Harga</TableHead>
|
||||||
<TableHead className="text-center">Stok</TableHead>
|
<TableHead className="text-center">Stok</TableHead>
|
||||||
{canAnyAction && (
|
<TableHead className="w-[100px] text-center">
|
||||||
<TableHead className="w-[100px] text-center">
|
Aksi
|
||||||
Aksi
|
</TableHead>
|
||||||
</TableHead>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{variants.length === 0 ? (
|
{variants.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={canAnyAction ? 6 : 5}
|
colSpan={6}
|
||||||
className="text-center text-muted-foreground"
|
className="text-center text-muted-foreground"
|
||||||
>
|
>
|
||||||
Tidak ada varian.
|
Tidak ada varian.
|
||||||
@ -105,39 +101,37 @@ export function RawMaterialVariantSubRow({
|
|||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{formatNumber(variant.stock)}
|
{formatNumber(variant.stock)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{canAnyAction && (
|
<TableCell>
|
||||||
<TableCell>
|
<RowActions
|
||||||
<RowActions
|
actions={[
|
||||||
actions={[
|
{
|
||||||
{
|
label: 'Edit',
|
||||||
label: 'Edit',
|
icon: (
|
||||||
icon: (
|
<Pencil className="h-4 w-4" />
|
||||||
<Pencil className="h-4 w-4" />
|
),
|
||||||
),
|
show: can('raw_materials.update'),
|
||||||
show: can('raw_materials.update'),
|
onClick: () => {
|
||||||
onClick: () => {
|
router.visit(
|
||||||
router.visit(
|
variantEdit.url({
|
||||||
variantEdit.url({
|
rawMaterial:
|
||||||
rawMaterial:
|
rawMaterial.id,
|
||||||
rawMaterial.id,
|
variant: variant.id,
|
||||||
variant: variant.id,
|
}),
|
||||||
}),
|
);
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
label: 'Hapus',
|
{
|
||||||
icon: (
|
label: 'Hapus',
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
icon: (
|
||||||
),
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
show: can('raw_materials.delete'),
|
),
|
||||||
onClick: () =>
|
show: can('raw_materials.delete'),
|
||||||
setDeletingVariant(variant),
|
onClick: () =>
|
||||||
},
|
setDeletingVariant(variant),
|
||||||
]}
|
},
|
||||||
/>
|
]}
|
||||||
</TableCell>
|
/>
|
||||||
)}
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export function createSupplierColumns(
|
|||||||
): ColumnDef<Supplier>[] {
|
): ColumnDef<Supplier>[] {
|
||||||
const { handleEdit, handleDeleteClick, can } = params;
|
const { handleEdit, handleDeleteClick, can } = params;
|
||||||
|
|
||||||
const columns: ColumnDef<Supplier>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: () => <span>Nama</span>,
|
header: () => <span>Nama</span>,
|
||||||
@ -46,10 +46,7 @@ export function createSupplierColumns(
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (can('suppliers.update') || can('suppliers.delete')) {
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -80,8 +77,6 @@ export function createSupplierColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
];
|
||||||
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export function createRoleColumns(
|
|||||||
): ColumnDef<Role>[] {
|
): ColumnDef<Role>[] {
|
||||||
const { handleEdit, handleDeleteClick, can } = params;
|
const { handleEdit, handleDeleteClick, can } = params;
|
||||||
|
|
||||||
const columns: ColumnDef<Role>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: () => <span>Nama Role</span>,
|
header: () => <span>Nama Role</span>,
|
||||||
@ -44,10 +44,7 @@ export function createRoleColumns(
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (can('roles.update') || can('roles.delete')) {
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -78,8 +75,6 @@ export function createRoleColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
];
|
||||||
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,8 +45,8 @@
|
|||||||
Route::delete('products/{product}/variants/{variant}', [ProductVariantController::class, 'destroy'])->name('products.variants.destroy')->middleware('permission:products.delete');
|
Route::delete('products/{product}/variants/{variant}', [ProductVariantController::class, 'destroy'])->name('products.variants.destroy')->middleware('permission:products.delete');
|
||||||
Route::get('products/{product}/variants/{variant}/edit', [ProductVariantController::class, 'edit'])->name('products.variants.edit')->middleware('permission:products.update');
|
Route::get('products/{product}/variants/{variant}/edit', [ProductVariantController::class, 'edit'])->name('products.variants.edit')->middleware('permission:products.update');
|
||||||
Route::put('products/{product}/variants/{variant}', [ProductVariantController::class, 'update'])->name('products.variants.update')->middleware('permission:products.update');
|
Route::put('products/{product}/variants/{variant}', [ProductVariantController::class, 'update'])->name('products.variants.update')->middleware('permission:products.update');
|
||||||
Route::post('products/{product}/variants/{variant}/transfer-stock', [ProductVariantController::class, 'transferStock'])->name('products.variants.transfer-stock')->middleware('permission:products.transfer_stock');
|
Route::post('products/{product}/variants/{variant}/transfer-stock', [ProductVariantController::class, 'transferStock'])->name('products.variants.transfer-stock')->middleware('permission:products.update');
|
||||||
Route::get('products/{product}/variants/{variant}/stock-mutations', [StockMutationController::class, 'index'])->name('products.variants.stock-mutations')->middleware('permission:products.view_stock_mutations');
|
Route::get('products/{product}/variants/{variant}/stock-mutations', [StockMutationController::class, 'index'])->name('products.variants.stock-mutations')->middleware('permission:products.view');
|
||||||
|
|
||||||
Route::resource('raw-materials', RawMaterialController::class)->except(['show'])->middleware('permission:raw_materials.view|raw_materials.create|raw_materials.update|raw_materials.delete');
|
Route::resource('raw-materials', RawMaterialController::class)->except(['show'])->middleware('permission:raw_materials.view|raw_materials.create|raw_materials.update|raw_materials.delete');
|
||||||
Route::post('raw-materials/{rawMaterial}/toggle-status', [RawMaterialController::class, 'toggleStatus'])->name('raw-materials.toggle-status')->middleware('permission:raw_materials.toggle_status');
|
Route::post('raw-materials/{rawMaterial}/toggle-status', [RawMaterialController::class, 'toggleStatus'])->name('raw-materials.toggle-status')->middleware('permission:raw_materials.toggle_status');
|
||||||
@ -101,8 +101,8 @@
|
|||||||
Route::post('employees/{user}/reset-password', [EmployeeController::class, 'resetPassword'])->name('employees.reset-password')->middleware('permission:employees.reset_password');
|
Route::post('employees/{user}/reset-password', [EmployeeController::class, 'resetPassword'])->name('employees.reset-password')->middleware('permission:employees.reset_password');
|
||||||
|
|
||||||
Route::get('attendances', [AttendanceController::class, 'index'])->name('attendances.index')->middleware('permission:attendances.view');
|
Route::get('attendances', [AttendanceController::class, 'index'])->name('attendances.index')->middleware('permission:attendances.view');
|
||||||
Route::post('attendances', [AttendanceController::class, 'store'])->name('attendances.store')->middleware('permission:attendances.create');
|
Route::post('attendances', [AttendanceController::class, 'store'])->name('attendances.store')->middleware('permission:attendances.manage');
|
||||||
Route::put('attendances/{attendance}', [AttendanceController::class, 'update'])->name('attendances.update')->middleware('permission:attendances.create');
|
Route::put('attendances/{attendance}', [AttendanceController::class, 'update'])->name('attendances.update')->middleware('permission:attendances.manage');
|
||||||
Route::get('attendances/by-date', [AttendanceController::class, 'byDate'])->name('attendances.by-date')->middleware('permission:attendances.view');
|
Route::get('attendances/by-date', [AttendanceController::class, 'byDate'])->name('attendances.by-date')->middleware('permission:attendances.view');
|
||||||
|
|
||||||
Route::resource('leave-requests', LeaveRequestController::class)->except(['show', 'create', 'edit'])->middleware('permission:leave_requests.view|leave_requests.create|leave_requests.update|leave_requests.delete');
|
Route::resource('leave-requests', LeaveRequestController::class)->except(['show', 'create', 'edit'])->middleware('permission:leave_requests.view|leave_requests.create|leave_requests.update|leave_requests.delete');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user