feat: add notification highlight feature to various services and frontend components

- Updated paginated methods in multiple services to accept a highlight parameter for filtering results.
- Modified notification URLs to include the highlight parameter for specific entity IDs.
- Enhanced frontend components to display a message when filtered by notification, with an option to show all entries.
- Implemented mark as read functionality in the notification bell component upon clicking a notification.
- Updated multiple index pages to handle the highlight prop and display relevant messages.
This commit is contained in:
Yoga Pangestu 2026-08-15 00:11:40 +07:00
parent aff7ddbcf2
commit 22c9a4cf2a
42 changed files with 455 additions and 81 deletions

View File

@ -32,6 +32,7 @@ public function index(PaginatedRequest $request): Response
'filterOptions' => [ 'filterOptions' => [
'typeOptions' => CashTransactionType::toSelect(), 'typeOptions' => CashTransactionType::toSelect(),
], ],
'highlight' => $request->input('highlight'),
]); ]);
} }

View File

@ -30,6 +30,7 @@ public function index(PaginatedRequest $request): Response
'filterOptions' => [ 'filterOptions' => [
'statusOptions' => EmployeeAdvanceStatus::toSelect(), 'statusOptions' => EmployeeAdvanceStatus::toSelect(),
], ],
'highlight' => $request->input('highlight'),
]); ]);
} }

View File

@ -21,6 +21,7 @@ public function index(PaginatedRequest $request): Response
{ {
return Inertia::render('admin/finance/expense/index', [ return Inertia::render('admin/finance/expense/index', [
'expenses' => $this->service->paginated(...$request->validatedWithDefaults()), 'expenses' => $this->service->paginated(...$request->validatedWithDefaults()),
'highlight' => $request->input('highlight'),
]); ]);
} }

View File

@ -24,6 +24,7 @@ public function index(PaginatedRequest $request): Response
{ {
return Inertia::render('admin/finance/payroll-period/index', [ return Inertia::render('admin/finance/payroll-period/index', [
'payrollPeriods' => $this->service->paginated(...$request->validatedWithDefaults()), 'payrollPeriods' => $this->service->paginated(...$request->validatedWithDefaults()),
'highlight' => $request->input('highlight'),
]); ]);
} }

View File

@ -24,6 +24,7 @@ public function index(Request $request): Response
return Inertia::render('admin/hr/attendance/index', [ return Inertia::render('admin/hr/attendance/index', [
...$this->service->getIndexData($year, $month), ...$this->service->getIndexData($year, $month),
'highlight' => $request->input('highlight'),
]); ]);
} }

View File

@ -30,6 +30,7 @@ public function index(PaginatedRequest $request): Response
'statusOptions' => LeaveRequestStatus::toSelect(), 'statusOptions' => LeaveRequestStatus::toSelect(),
], ],
'currentUserId' => $request->user()->id, 'currentUserId' => $request->user()->id,
'highlight' => $request->input('highlight'),
]); ]);
} }

View File

@ -33,6 +33,7 @@ public function index(PaginatedRequest $request): Response
'statusOptions' => CuttingStatus::toSelect(), 'statusOptions' => CuttingStatus::toSelect(),
'productNames' => $this->service->getProductNames(), 'productNames' => $this->service->getProductNames(),
], ],
'highlight' => $request->input('highlight'),
]); ]);
} }

View File

@ -31,6 +31,7 @@ public function index(PaginatedRequest $request): Response
), ),
'suppliers' => $this->supplierService->getAll(), 'suppliers' => $this->supplierService->getAll(),
'filters' => $request->only(['supplier_id']), 'filters' => $request->only(['supplier_id']),
'highlight' => $request->input('highlight'),
]); ]);
} }

View File

@ -26,6 +26,7 @@ public function index(PaginatedRequest $request): Response
'restocks' => $this->service->paginated( 'restocks' => $this->service->paginated(
...$request->validatedWithDefaults(), ...$request->validatedWithDefaults(),
), ),
'highlight' => $request->input('highlight'),
]); ]);
} }

View File

@ -40,6 +40,7 @@ public function index(PaginatedRequest $request): Response
'summary' => $this->service->getSummary($filters, $user), 'summary' => $this->service->getSummary($filters, $user),
'filters' => $filters, 'filters' => $filters,
'filterOptions' => $this->service->getFilterOptions(), 'filterOptions' => $this->service->getFilterOptions(),
'highlight' => $request->input('highlight'),
]); ]);
} }

View File

@ -30,6 +30,7 @@ public function index(PaginatedRequest $request): Response
'categories' => $this->categoryService->getAll(), 'categories' => $this->categoryService->getAll(),
'productNames' => $this->service->getNames(), 'productNames' => $this->service->getNames(),
'filters' => $request->only(['status', 'stock', 'category', 'name', 'featured']), 'filters' => $request->only(['status', 'stock', 'category', 'name', 'featured']),
'highlight' => $request->input('highlight'),
]); ]);
} }

View File

@ -27,6 +27,7 @@ public function index(PaginatedRequest $request): Response
), ),
'rawMaterialNames' => $this->service->getNames(), 'rawMaterialNames' => $this->service->getNames(),
'filters' => $request->only(['is_active', 'stock', 'name']), 'filters' => $request->only(['is_active', 'stock', 'name']),
'highlight' => $request->input('highlight'),
]); ]);
} }

View File

@ -26,11 +26,17 @@ public function validatedWithDefaults(): array
{ {
$validated = $this->validated(); $validated = $this->validated();
return [ $result = [
'perPage' => $validated['per_page'] ?? 25, 'perPage' => $validated['per_page'] ?? 25,
'search' => $validated['search'] ?? '', 'search' => $validated['search'] ?? '',
'sort' => $validated['sort'] ?? 'created_at', 'sort' => $validated['sort'] ?? 'created_at',
'direction' => $validated['direction'] ?? 'desc', 'direction' => $validated['direction'] ?? 'desc',
]; ];
if (! empty($validated['highlight'])) {
$result['highlight'] = (int) $validated['highlight'];
}
return $result;
} }
} }

View File

@ -25,7 +25,7 @@ public function __construct(
private CashAccountService $cashAccountService private CashAccountService $cashAccountService
) {} ) {}
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?int $highlight = null): LengthAwarePaginator
{ {
$cashAccount = $this->cashAccountService->get(); $cashAccount = $this->cashAccountService->get();
@ -36,6 +36,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
$paginator = $cashAccount->cashTransactions() $paginator = $cashAccount->cashTransactions()
->select(['id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at']) ->select(['id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at'])
->with('createdBy.userProfile', 'media') ->with('createdBy.userProfile', 'media')
->when($highlight, fn ($q) => $q->where('id', $highlight))
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%")) ->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
->when($filters['type'] ?? null, function ($query, $type) { ->when($filters['type'] ?? null, function ($query, $type) {
$query->where('type', $type); $query->where('type', $type);
@ -63,7 +64,7 @@ public function deposit(array $data): CashTransaction
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Setoran Kas Toko', title: 'Setoran Kas Toko',
body: 'Setoran sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.', body: 'Setoran sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.',
url: route('admin.finance.cash-accounts.index'), url: route('admin.finance.cash-accounts.index', ['highlight' => $transaction->id]),
); );
return $transaction; return $transaction;
@ -85,7 +86,7 @@ public function withdrawal(array $data): CashTransaction
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Penarikan Kas Toko', title: 'Penarikan Kas Toko',
body: 'Penarikan sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.', body: 'Penarikan sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.',
url: route('admin.finance.cash-accounts.index'), url: route('admin.finance.cash-accounts.index', ['highlight' => $transaction->id]),
); );
return $transaction; return $transaction;
@ -133,7 +134,7 @@ public function update(CashTransaction $transaction, array $data): CashTransacti
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Transaksi Kas Diperbarui', title: 'Transaksi Kas Diperbarui',
body: 'Transaksi kas sebesar Rp '.number_format($transaction->amount, 0, ',', '.').' berhasil diperbarui'.' oleh '.auth()->user()->full_name.'.', body: 'Transaksi kas sebesar Rp '.number_format($transaction->amount, 0, ',', '.').' berhasil diperbarui'.' oleh '.auth()->user()->full_name.'.',
url: route('admin.finance.cash-accounts.index'), url: route('admin.finance.cash-accounts.index', ['highlight' => $transaction->id]),
); );
return $transaction; return $transaction;

View File

@ -18,11 +18,12 @@ class EmployeeAdvanceService
{ {
use HandlesCashTransactions, HasRoleChecks; use HandlesCashTransactions, HasRoleChecks;
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?int $highlight = null): LengthAwarePaginator
{ {
return EmployeeAdvance::query() return EmployeeAdvance::query()
->select(['id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at']) ->select(['id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at'])
->with(['employee.user.userProfile', 'payments.paidBy.userProfile']) ->with(['employee.user.userProfile', 'payments.paidBy.userProfile'])
->when($highlight, fn ($q) => $q->where('id', $highlight))
->when(! self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))) ->when(! self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
->when($filters['status'] ?? null, fn ($q) => $q->where('status', $filters['status'])) ->when($filters['status'] ?? null, fn ($q) => $q->where('status', $filters['status']))
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%")) ->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
@ -54,7 +55,7 @@ public function store(array $data): EmployeeAdvance
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Kasbon Baru', title: 'Kasbon Baru',
body: 'Kasbon sebesar Rp '.number_format($data['amount'], 0, ',', '.')." dari {$employeeAdvance->employee->name} menunggu persetujuan".' oleh '.auth()->user()->full_name.'.', body: 'Kasbon sebesar Rp '.number_format($data['amount'], 0, ',', '.')." dari {$employeeAdvance->employee->name} menunggu persetujuan".' oleh '.auth()->user()->full_name.'.',
url: route('admin.finance.employee-advances.index'), url: route('admin.finance.employee-advances.index', ['highlight' => $employeeAdvance->id]),
additionalUser: $employeeAdvance->employee->user ?? null, additionalUser: $employeeAdvance->employee->user ?? null,
); );
@ -171,7 +172,7 @@ public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Kasbon Dikeluarkan', title: 'Kasbon Dikeluarkan',
body: 'Kasbon sebesar Rp '.number_format($employeeAdvance->amount, 0, ',', '.').' telah disetujui dan dikeluarkan'.' oleh '.auth()->user()->full_name.'.', body: 'Kasbon sebesar Rp '.number_format($employeeAdvance->amount, 0, ',', '.').' telah disetujui dan dikeluarkan'.' oleh '.auth()->user()->full_name.'.',
url: route('admin.finance.employee-advances.index'), url: route('admin.finance.employee-advances.index', ['highlight' => $employeeAdvance->id]),
additionalUser: $employeeAdvance->employee->user ?? null, additionalUser: $employeeAdvance->employee->user ?? null,
); );
@ -224,7 +225,7 @@ public function pay(EmployeeAdvance $employeeAdvance, int $amount): EmployeeAdva
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: $employeeAdvance->status === EmployeeAdvanceStatus::REPAID ? 'Kasbon Dikembalikan' : 'Pembayaran Kasbon', title: $employeeAdvance->status === EmployeeAdvanceStatus::REPAID ? 'Kasbon Dikembalikan' : 'Pembayaran Kasbon',
body: $notificationBody, body: $notificationBody,
url: route('admin.finance.employee-advances.index'), url: route('admin.finance.employee-advances.index', ['highlight' => $employeeAdvance->id]),
additionalUser: $employeeAdvance->employee->user ?? null, additionalUser: $employeeAdvance->employee->user ?? null,
); );

View File

@ -23,11 +23,12 @@ public function __construct(
private S3PresignedService $s3Service, private S3PresignedService $s3Service,
) {} ) {}
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?int $highlight = null): LengthAwarePaginator
{ {
$paginator = Expense::query() $paginator = Expense::query()
->select(['id', 'created_by_id', 'amount', 'description', 'created_at']) ->select(['id', 'created_by_id', 'amount', 'description', 'created_at'])
->with('createdBy.userProfile', 'media') ->with('createdBy.userProfile', 'media')
->when($highlight, fn ($q) => $q->where('id', $highlight))
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%")) ->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
->orderBy($sort, $direction) ->orderBy($sort, $direction)
->paginate($perPage); ->paginate($perPage);
@ -63,7 +64,7 @@ public function store(array $data): Expense
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Pengeluaran Baru', title: 'Pengeluaran Baru',
body: 'Pengeluaran sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.', body: 'Pengeluaran sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.',
url: route('admin.finance.expenses.index'), url: route('admin.finance.expenses.index', ['highlight' => $expense->id]),
); );
return $expense; return $expense;
@ -110,7 +111,7 @@ public function update(Expense $expense, array $data): Expense
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Pengeluaran Diperbarui', title: 'Pengeluaran Diperbarui',
body: 'Pengeluaran sebesar Rp '.number_format($expense->amount, 0, ',', '.').' berhasil diperbarui'.' oleh '.auth()->user()->full_name.'.', body: 'Pengeluaran sebesar Rp '.number_format($expense->amount, 0, ',', '.').' berhasil diperbarui'.' oleh '.auth()->user()->full_name.'.',
url: route('admin.finance.expenses.index'), url: route('admin.finance.expenses.index', ['highlight' => $expense->id]),
); );
return $expense; return $expense;

View File

@ -19,10 +19,11 @@ class PayrollPeriodService
{ {
use HandlesCashTransactions, HasRoleChecks; use HandlesCashTransactions, HasRoleChecks;
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?int $highlight = null): LengthAwarePaginator
{ {
return PayrollPeriod::query() return PayrollPeriod::query()
->select(['id', 'year', 'month', 'status', 'closed_at', 'created_at']) ->select(['id', 'year', 'month', 'status', 'closed_at', 'created_at'])
->when($highlight, fn ($q) => $q->where('id', $highlight))
->when(! self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]), function ($query) { ->when(! self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]), function ($query) {
$query->withCount(['payrolls as payrolls_count' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))]) $query->withCount(['payrolls as payrolls_count' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
->withSum(['payrolls as payrolls_sum_total_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'total_amount') ->withSum(['payrolls as payrolls_sum_total_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'total_amount')
@ -148,7 +149,7 @@ public function pay(Payroll $payroll): Payroll
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Gaji Dibayar', title: 'Gaji Dibayar',
body: "Gaji {$employeeUser->full_name} sebesar {$payroll->formatted_amount} telah dibayar oleh " . auth()->user()->full_name . '.', body: "Gaji {$employeeUser->full_name} sebesar {$payroll->formatted_amount} telah dibayar oleh " . auth()->user()->full_name . '.',
url: route('admin.finance.payroll-periods.index'), url: route('admin.finance.payroll-periods.index', ['highlight' => $payroll->payroll_period_id]),
additionalUser: $employeeUser, additionalUser: $employeeUser,
); );
@ -179,7 +180,7 @@ public function cancel(Payroll $payroll): Payroll
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Gaji Dibatalkan', title: 'Gaji Dibatalkan',
body: "Gaji {$employeeUser->full_name} sebesar {$payroll->formatted_amount} telah dibatalkan".' oleh '.auth()->user()->full_name.'.', body: "Gaji {$employeeUser->full_name} sebesar {$payroll->formatted_amount} telah dibatalkan".' oleh '.auth()->user()->full_name.'.',
url: route('admin.finance.payroll-periods.index'), url: route('admin.finance.payroll-periods.index', ['highlight' => $payroll->payroll_period_id]),
additionalUser: $employeeUser, additionalUser: $employeeUser,
); );

View File

@ -175,7 +175,7 @@ public function checkIn(array $data): Attendance
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
title: 'Presensi Masuk', title: 'Presensi Masuk',
body: 'Presensi masuk oleh '.auth()->user()->full_name.'.', body: 'Presensi masuk oleh '.auth()->user()->full_name.'.',
url: route('admin.hr.attendances.index'), url: route('admin.hr.attendances.index', ['highlight' => $attendance->id]),
); );
return $attendance; return $attendance;
@ -197,7 +197,7 @@ public function checkOut(Attendance $attendance, array $data): Attendance
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
title: 'Presensi Pulang', title: 'Presensi Pulang',
body: 'Presensi pulang oleh '.auth()->user()->full_name.'.', body: 'Presensi pulang oleh '.auth()->user()->full_name.'.',
url: route('admin.hr.attendances.index'), url: route('admin.hr.attendances.index', ['highlight' => $attendance->id]),
); );
return $attendance; return $attendance;

View File

@ -16,11 +16,12 @@ class LeaveRequestService
{ {
use HasRoleChecks; use HasRoleChecks;
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?int $highlight = null): LengthAwarePaginator
{ {
return LeaveRequest::query() return LeaveRequest::query()
->select(['id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at']) ->select(['id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at'])
->with(['employee.user.userProfile']) ->with(['employee.user.userProfile'])
->when($highlight, fn ($q) => $q->where('id', $highlight))
->when(! self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))) ->when(! self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]), 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) {
@ -60,7 +61,7 @@ public function store(array $data): LeaveRequest
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Pengajuan Cuti Baru', title: 'Pengajuan Cuti Baru',
body: "Pengajuan cuti {$leaveRequest->total_days} hari oleh ".auth()->user()->full_name.'.', body: "Pengajuan cuti {$leaveRequest->total_days} hari oleh ".auth()->user()->full_name.'.',
url: route('admin.hr.leave-requests.index'), url: route('admin.hr.leave-requests.index', ['highlight' => $leaveRequest->id]),
additionalUser: $leaveRequest->employee->user ?? null, additionalUser: $leaveRequest->employee->user ?? null,
); );
@ -115,7 +116,7 @@ public function approve(LeaveRequest $leaveRequest): LeaveRequest
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Cuti Disetujui', title: 'Cuti Disetujui',
body: "Cuti {$leaveRequest->employee->name} telah disetujui oleh ".auth()->user()->full_name.'.', body: "Cuti {$leaveRequest->employee->name} telah disetujui oleh ".auth()->user()->full_name.'.',
url: route('admin.hr.leave-requests.index'), url: route('admin.hr.leave-requests.index', ['highlight' => $leaveRequest->id]),
additionalUser: $employeeUser, additionalUser: $employeeUser,
); );
@ -136,7 +137,7 @@ public function reject(LeaveRequest $leaveRequest): LeaveRequest
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Cuti Ditolak', title: 'Cuti Ditolak',
body: "Cuti {$leaveRequest->employee->name} telah ditolak oleh ".auth()->user()->full_name.'.', body: "Cuti {$leaveRequest->employee->name} telah ditolak oleh ".auth()->user()->full_name.'.',
url: route('admin.hr.leave-requests.index'), url: route('admin.hr.leave-requests.index', ['highlight' => $leaveRequest->id]),
additionalUser: $employeeUser, additionalUser: $employeeUser,
); );

View File

@ -25,7 +25,7 @@ public function __construct(
private S3PresignedService $s3Service, private S3PresignedService $s3Service,
) {} ) {}
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?int $highlight = null): LengthAwarePaginator
{ {
$materialsCountQuery = '(SELECT COUNT(*) FROM cutting_materials WHERE cutting_materials.cutting_id = cuttings.id AND cutting_materials.deleted_at IS NULL)'; $materialsCountQuery = '(SELECT COUNT(*) FROM cutting_materials WHERE cutting_materials.cutting_id = cuttings.id AND cutting_materials.deleted_at IS NULL)';
$totalUsageQuery = '(SELECT IFNULL(SUM(material_usage), 0) FROM cutting_materials WHERE cutting_materials.cutting_id = cuttings.id AND cutting_materials.deleted_at IS NULL)'; $totalUsageQuery = '(SELECT IFNULL(SUM(material_usage), 0) FROM cutting_materials WHERE cutting_materials.cutting_id = cuttings.id AND cutting_materials.deleted_at IS NULL)';
@ -42,6 +42,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
->selectRaw("{$totalUsageQuery} as total_usage") ->selectRaw("{$totalUsageQuery} as total_usage")
->selectRaw("{$productNameQuery} as product_name") ->selectRaw("{$productNameQuery} as product_name")
->selectRaw("{$cuttingResultQuery} as cutting_result") ->selectRaw("{$cuttingResultQuery} as cutting_result")
->when($highlight, fn ($q) => $q->where('id', $highlight))
->when($search, function ($q) use ($search) { ->when($search, function ($q) use ($search) {
$q->whereHas('cuttingResults', fn ($rq) => $rq->where('product_name', 'like', "%{$search}%")) $q->whereHas('cuttingResults', fn ($rq) => $rq->where('product_name', 'like', "%{$search}%"))
->orWhere('description', 'like', "%{$search}%"); ->orWhere('description', 'like', "%{$search}%");
@ -365,7 +366,7 @@ public function store(array $data): Cutting
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
title: 'Cutting Baru', title: 'Cutting Baru',
body: 'Cutting berhasil ditambahkan oleh '.auth()->user()->full_name.'.', body: 'Cutting berhasil ditambahkan oleh '.auth()->user()->full_name.'.',
url: route('admin.manage.cuttings.index'), url: route('admin.manage.cuttings.index', ['highlight' => $cutting->id]),
); );
return $cutting; return $cutting;
@ -492,7 +493,7 @@ public function update(Cutting $cutting, array $data): Cutting
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
title: 'Cutting Diperbarui', title: 'Cutting Diperbarui',
body: 'Cutting berhasil diperbarui oleh '.auth()->user()->full_name.'.', body: 'Cutting berhasil diperbarui oleh '.auth()->user()->full_name.'.',
url: route('admin.manage.cuttings.index'), url: route('admin.manage.cuttings.index', ['highlight' => $cutting->id]),
); );
return $cutting; return $cutting;
@ -538,7 +539,7 @@ public function complete(Cutting $cutting): Cutting
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
title: 'Cutting Selesai', title: 'Cutting Selesai',
body: 'Cutting berhasil diselesaikan oleh '.auth()->user()->full_name.'.', body: 'Cutting berhasil diselesaikan oleh '.auth()->user()->full_name.'.',
url: route('admin.manage.cuttings.index'), url: route('admin.manage.cuttings.index', ['highlight' => $cutting->id]),
); );
return $cutting; return $cutting;

View File

@ -23,7 +23,7 @@ public function __construct(
private S3PresignedService $s3Service, private S3PresignedService $s3Service,
) {} ) {}
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?int $highlight = null): LengthAwarePaginator
{ {
$itemCountQuery = '(SELECT COUNT(*) FROM purchase_items WHERE purchase_items.purchase_id = purchases.id AND purchase_items.deleted_at IS NULL)'; $itemCountQuery = '(SELECT COUNT(*) FROM purchase_items WHERE purchase_items.purchase_id = purchases.id AND purchase_items.deleted_at IS NULL)';
$totalQtyQuery = '(SELECT IFNULL(SUM(quantity), 0) FROM purchase_items WHERE purchase_items.purchase_id = purchases.id AND purchase_items.deleted_at IS NULL)'; $totalQtyQuery = '(SELECT IFNULL(SUM(quantity), 0) FROM purchase_items WHERE purchase_items.purchase_id = purchases.id AND purchase_items.deleted_at IS NULL)';
@ -41,6 +41,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
->selectRaw("{$totalQtyQuery} as total_qty") ->selectRaw("{$totalQtyQuery} as total_qty")
->selectRaw("{$materialNameQuery} as material_name") ->selectRaw("{$materialNameQuery} as material_name")
->selectRaw("{$unitQuery} as unit") ->selectRaw("{$unitQuery} as unit")
->when($highlight, fn ($q) => $q->where('id', $highlight))
->when($search, function ($q) use ($search) { ->when($search, function ($q) use ($search) {
$q->whereHas('supplier', fn ($sq) => $sq->where('name', 'like', "%{$search}%")) $q->whereHas('supplier', fn ($sq) => $sq->where('name', 'like', "%{$search}%"))
->orWhere('notes', 'like', "%{$search}%"); ->orWhere('notes', 'like', "%{$search}%");
@ -238,7 +239,7 @@ private function storeFromExisting(array $data): Purchase
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
title: 'Belanja Baru', title: 'Belanja Baru',
body: 'Belanja bahan baku sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.', body: 'Belanja bahan baku sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
url: route('admin.manage.purchases.index'), url: route('admin.manage.purchases.index', ['highlight' => $purchase->id]),
); );
return $purchase; return $purchase;
@ -335,7 +336,7 @@ private function storeNew(array $data): Purchase
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
title: 'Belanja Baru', title: 'Belanja Baru',
body: 'Belanja bahan baku sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.', body: 'Belanja bahan baku sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
url: route('admin.manage.purchases.index'), url: route('admin.manage.purchases.index', ['highlight' => $purchase->id]),
); );
return $purchase; return $purchase;
@ -482,7 +483,7 @@ public function update(Purchase $purchase, array $data): Purchase
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
title: 'Belanja Diperbarui', title: 'Belanja Diperbarui',
body: 'Belanja bahan baku sebesar '.$purchase->formatted_total.' berhasil diperbarui oleh '.auth()->user()->full_name.'.', body: 'Belanja bahan baku sebesar '.$purchase->formatted_total.' berhasil diperbarui oleh '.auth()->user()->full_name.'.',
url: route('admin.manage.purchases.index'), url: route('admin.manage.purchases.index', ['highlight' => $purchase->id]),
); );
return $purchase; return $purchase;

View File

@ -23,7 +23,7 @@ public function __construct(
private S3PresignedService $s3Service, private S3PresignedService $s3Service,
) {} ) {}
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?int $highlight = null): LengthAwarePaginator
{ {
$itemsCountQuery = '(SELECT COUNT(*) FROM restock_items WHERE restock_items.restock_id = restocks.id AND restock_items.deleted_at IS NULL)'; $itemsCountQuery = '(SELECT COUNT(*) FROM restock_items WHERE restock_items.restock_id = restocks.id AND restock_items.deleted_at IS NULL)';
$totalQtyQuery = '(SELECT IFNULL(SUM(quantity), 0) FROM restock_items WHERE restock_items.restock_id = restocks.id AND restock_items.deleted_at IS NULL)'; $totalQtyQuery = '(SELECT IFNULL(SUM(quantity), 0) FROM restock_items WHERE restock_items.restock_id = restocks.id AND restock_items.deleted_at IS NULL)';
@ -38,6 +38,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
->selectRaw("{$itemsCountQuery} as items_count") ->selectRaw("{$itemsCountQuery} as items_count")
->selectRaw("{$totalQtyQuery} as total_qty") ->selectRaw("{$totalQtyQuery} as total_qty")
->selectRaw("{$productNamesQuery} as product_names") ->selectRaw("{$productNamesQuery} as product_names")
->when($highlight, fn ($q) => $q->where('id', $highlight))
->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}%");
@ -98,7 +99,7 @@ public function store(array $data): Restock
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
title: 'Restock Baru', title: 'Restock Baru',
body: 'Restock '.($stockType === ProductStockQuality::GOOD->value ? 'produk' : 'reject').' sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.', body: 'Restock '.($stockType === ProductStockQuality::GOOD->value ? 'produk' : 'reject').' sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
url: route('admin.manage.restocks.index'), url: route('admin.manage.restocks.index', ['highlight' => $restock->id]),
); );
return $restock; return $restock;
@ -143,7 +144,7 @@ public function update(Restock $restock, array $data): Restock
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
title: 'Restock Diperbarui', title: 'Restock Diperbarui',
body: 'Restock berhasil diperbarui oleh '.auth()->user()->full_name.'.', body: 'Restock berhasil diperbarui oleh '.auth()->user()->full_name.'.',
url: route('admin.manage.restocks.index'), url: route('admin.manage.restocks.index', ['highlight' => $restock->id]),
); );
return $restock; return $restock;

View File

@ -39,7 +39,7 @@ public function __construct(
private S3PresignedService $s3Service, private S3PresignedService $s3Service,
) {} ) {}
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?User $user = null): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?User $user = null, ?int $highlight = null): LengthAwarePaginator
{ {
$itemsCountQuery = '(SELECT COUNT(*) FROM order_items WHERE order_items.order_id = orders.id AND order_items.deleted_at IS NULL)'; $itemsCountQuery = '(SELECT COUNT(*) FROM order_items WHERE order_items.order_id = orders.id AND order_items.deleted_at IS NULL)';
$totalQtyQuery = '(SELECT IFNULL(SUM(quantity), 0) FROM order_items WHERE order_items.order_id = orders.id AND order_items.deleted_at IS NULL)'; $totalQtyQuery = '(SELECT IFNULL(SUM(quantity), 0) FROM order_items WHERE order_items.order_id = orders.id AND order_items.deleted_at IS NULL)';
@ -57,6 +57,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
->selectRaw("{$itemsCountQuery} as items_count") ->selectRaw("{$itemsCountQuery} as items_count")
->selectRaw("{$totalQtyQuery} as total_qty") ->selectRaw("{$totalQtyQuery} as total_qty")
->selectRaw("{$productNamesQuery} as product_names") ->selectRaw("{$productNamesQuery} as product_names")
->when($highlight, fn ($q) => $q->where('id', $highlight))
->when($user && $this->isMarketingUser($user), fn ($q) => $q->where('marketing_id', $user->id)) ->when($user && $this->isMarketingUser($user), fn ($q) => $q->where('marketing_id', $user->id))
->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}%"))
@ -263,7 +264,7 @@ public function store(array $data): Order
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
title: 'Transaksi Baru', title: 'Transaksi Baru',
body: 'Transaksi '.$order->order_number.' sebesar Rp '.$order->formatted_total_amount.' berhasil dicatat oleh '.auth()->user()->full_name.'.', body: 'Transaksi '.$order->order_number.' sebesar Rp '.$order->formatted_total_amount.' berhasil dicatat oleh '.auth()->user()->full_name.'.',
url: route('admin.manage.transactions.index'), url: route('admin.manage.transactions.index', ['highlight' => $order->id]),
); );
return $order; return $order;
@ -339,7 +340,7 @@ public function update(Order $order, array $data): Order
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
title: 'Transaksi Diperbarui', title: 'Transaksi Diperbarui',
body: 'Transaksi '.$order->order_number.' sebesar '.$order->formatted_total_amount.' berhasil diperbarui oleh '.auth()->user()->full_name.'.', body: 'Transaksi '.$order->order_number.' sebesar '.$order->formatted_total_amount.' berhasil diperbarui oleh '.auth()->user()->full_name.'.',
url: route('admin.manage.transactions.index'), url: route('admin.manage.transactions.index', ['highlight' => $order->id]),
); );
return $order; return $order;

View File

@ -32,7 +32,7 @@ public function getNames(): Collection
->pluck('name'); ->pluck('name');
} }
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?int $highlight = null): LengthAwarePaginator
{ {
$stockSumQuery = '(SELECT IFNULL(SUM(stock), 0) FROM product_variants WHERE product_variants.product_id = products.id AND product_variants.deleted_at IS NULL)'; $stockSumQuery = '(SELECT IFNULL(SUM(stock), 0) FROM product_variants WHERE product_variants.product_id = products.id AND product_variants.deleted_at IS NULL)';
$rejectSumQuery = '(SELECT IFNULL(SUM(reject_stock), 0) FROM product_variants WHERE product_variants.product_id = products.id AND product_variants.deleted_at IS NULL)'; $rejectSumQuery = '(SELECT IFNULL(SUM(reject_stock), 0) FROM product_variants WHERE product_variants.product_id = products.id AND product_variants.deleted_at IS NULL)';
@ -45,6 +45,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
->selectRaw("{$stockSumQuery} as total_stock") ->selectRaw("{$stockSumQuery} as total_stock")
->selectRaw("{$rejectSumQuery} as total_reject_stock") ->selectRaw("{$rejectSumQuery} as total_reject_stock")
->selectRaw("{$retailSumQuery} as total_retail_stock") ->selectRaw("{$retailSumQuery} as total_retail_stock")
->when($highlight, fn ($q) => $q->where('id', $highlight))
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%") ->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")
->orWhereHas('productVariants', fn ($vq) => $vq->where('name', 'like', "%{$search}%")) ->orWhereHas('productVariants', fn ($vq) => $vq->where('name', 'like', "%{$search}%"))
) )
@ -157,7 +158,7 @@ public function store(array $data): Product
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Produk Baru', title: 'Produk Baru',
body: "Produk \"{$product->name}\" berhasil ditambahkan".' oleh '.auth()->user()->full_name.'.', body: "Produk \"{$product->name}\" berhasil ditambahkan".' oleh '.auth()->user()->full_name.'.',
url: route('admin.master.products.index'), url: route('admin.master.products.index', ['highlight' => $product->id]),
); );
return $product; return $product;
@ -401,7 +402,7 @@ public function update(Product $product, array $data): Product
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Produk Diperbarui', title: 'Produk Diperbarui',
body: "Produk \"{$product->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.', body: "Produk \"{$product->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.',
url: route('admin.master.products.index'), url: route('admin.master.products.index', ['highlight' => $product->id]),
); );
return $product; return $product;
@ -446,7 +447,7 @@ public function toggleStatus(Product $product): void
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Status Produk Diubah', title: 'Status Produk Diubah',
body: "Produk \"{$product->name}\" berhasil {$status}".' oleh '.auth()->user()->full_name.'.', body: "Produk \"{$product->name}\" berhasil {$status}".' oleh '.auth()->user()->full_name.'.',
url: route('admin.master.products.index'), url: route('admin.master.products.index', ['highlight' => $product->id]),
); );
} }
@ -464,7 +465,7 @@ public function toggleFeatured(Product $product): void
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Status Unggulan Produk Diubah', title: 'Status Unggulan Produk Diubah',
body: "Produk \"{$product->name}\" berhasil {$status}".' oleh '.auth()->user()->full_name.'.', body: "Produk \"{$product->name}\" berhasil {$status}".' oleh '.auth()->user()->full_name.'.',
url: route('admin.master.products.index'), url: route('admin.master.products.index', ['highlight' => $product->id]),
); );
} }
@ -478,7 +479,7 @@ public function approve(Product $product): void
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
title: 'Produk Disetujui', title: 'Produk Disetujui',
body: "Produk \"{$product->name}\" telah disetujui oleh ".auth()->user()->full_name.'.', body: "Produk \"{$product->name}\" telah disetujui oleh ".auth()->user()->full_name.'.',
url: route('admin.master.products.index'), url: route('admin.master.products.index', ['highlight' => $product->id]),
additionalUser: $product->createdBy ?? null, additionalUser: $product->createdBy ?? null,
); );
} }
@ -494,7 +495,7 @@ public function reject(Product $product, string $reason = ''): void
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
title: 'Produk Ditolak', title: 'Produk Ditolak',
body: "Produk \"{$product->name}\" telah ditolak oleh ".auth()->user()->full_name.'.', body: "Produk \"{$product->name}\" telah ditolak oleh ".auth()->user()->full_name.'.',
url: route('admin.master.products.index'), url: route('admin.master.products.index', ['highlight' => $product->id]),
additionalUser: $product->createdBy ?? null, additionalUser: $product->createdBy ?? null,
); );
} }
@ -510,7 +511,7 @@ public function resubmit(Product $product): void
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
title: 'Produk Diajukan Ulang', title: 'Produk Diajukan Ulang',
body: "Produk \"{$product->name}\" telah diajukan ulang oleh ".auth()->user()->full_name.'.', body: "Produk \"{$product->name}\" telah diajukan ulang oleh ".auth()->user()->full_name.'.',
url: route('admin.master.products.index'), url: route('admin.master.products.index', ['highlight' => $product->id]),
); );
} }

View File

@ -144,7 +144,7 @@ public function update(ProductVariant $variant, array $data): ProductVariant
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Varian Diperbarui', title: 'Varian Diperbarui',
body: "Varian \"{$variant->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.', body: "Varian \"{$variant->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.',
url: route('admin.master.products.index'), url: route('admin.master.products.index', ['highlight' => $variant->product_id]),
); );
return $variant->fresh(); return $variant->fresh();
@ -206,7 +206,7 @@ public function transferStock(ProductVariant $variant, array $data): ProductVari
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Transfer Stok', title: 'Transfer Stok',
body: "{$quantity} unit dari varian \"{$variant->name}\" berhasil ditransfer dari stok bagus ke stok ecer".' oleh '.auth()->user()->full_name.'.', body: "{$quantity} unit dari varian \"{$variant->name}\" berhasil ditransfer dari stok bagus ke stok ecer".' oleh '.auth()->user()->full_name.'.',
url: route('admin.master.products.index'), url: route('admin.master.products.index', ['highlight' => $variant->product_id]),
); );
return $variant->fresh(); return $variant->fresh();

View File

@ -27,7 +27,7 @@ public function getNames(): Collection
->pluck('name'); ->pluck('name');
} }
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?int $highlight = null): LengthAwarePaginator
{ {
$stockSumQuery = '(SELECT IFNULL(SUM(stock), 0) FROM raw_material_prices WHERE raw_material_prices.raw_material_id = raw_materials.id AND raw_material_prices.deleted_at IS NULL)'; $stockSumQuery = '(SELECT IFNULL(SUM(stock), 0) FROM raw_material_prices WHERE raw_material_prices.raw_material_id = raw_materials.id AND raw_material_prices.deleted_at IS NULL)';
$valueSumQuery = '(SELECT IFNULL(SUM(price * stock), 0) FROM raw_material_prices WHERE raw_material_prices.raw_material_id = raw_materials.id AND raw_material_prices.deleted_at IS NULL)'; $valueSumQuery = '(SELECT IFNULL(SUM(price * stock), 0) FROM raw_material_prices WHERE raw_material_prices.raw_material_id = raw_materials.id AND raw_material_prices.deleted_at IS NULL)';
@ -37,6 +37,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
->withCount('rawMaterialPrices as variants_count') ->withCount('rawMaterialPrices as variants_count')
->selectRaw("{$stockSumQuery} as total_stock") ->selectRaw("{$stockSumQuery} as total_stock")
->selectRaw("{$valueSumQuery} as total_value") ->selectRaw("{$valueSumQuery} as total_value")
->when($highlight, fn ($q) => $q->where('id', $highlight))
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%") ->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")
->orWhereHas('rawMaterialPrices', fn ($vq) => $vq->where('variant', 'like', "%{$search}%")) ->orWhereHas('rawMaterialPrices', fn ($vq) => $vq->where('variant', 'like', "%{$search}%"))
) )
@ -110,7 +111,7 @@ public function store(array $data): RawMaterial
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Bahan Baku Baru', title: 'Bahan Baku Baru',
body: "Bahan baku \"{$rawMaterial->name}\" berhasil ditambahkan".' oleh '.auth()->user()->full_name.'.', body: "Bahan baku \"{$rawMaterial->name}\" berhasil ditambahkan".' oleh '.auth()->user()->full_name.'.',
url: route('admin.master.raw-materials.index'), url: route('admin.master.raw-materials.index', ['highlight' => $rawMaterial->id]),
); );
return $rawMaterial; return $rawMaterial;
@ -230,7 +231,7 @@ public function update(RawMaterial $rawMaterial, array $data): RawMaterial
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Bahan Baku Diperbarui', title: 'Bahan Baku Diperbarui',
body: "Bahan baku \"{$rawMaterial->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.', body: "Bahan baku \"{$rawMaterial->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.',
url: route('admin.master.raw-materials.index'), url: route('admin.master.raw-materials.index', ['highlight' => $rawMaterial->id]),
); );
return $rawMaterial; return $rawMaterial;
@ -268,7 +269,7 @@ public function toggleStatus(RawMaterial $rawMaterial): void
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Status Bahan Baku Diubah', title: 'Status Bahan Baku Diubah',
body: "Bahan baku \"{$rawMaterial->name}\" berhasil {$status}".' oleh '.auth()->user()->full_name.'.', body: "Bahan baku \"{$rawMaterial->name}\" berhasil {$status}".' oleh '.auth()->user()->full_name.'.',
url: route('admin.master.raw-materials.index'), url: route('admin.master.raw-materials.index', ['highlight' => $rawMaterial->id]),
); );
} }
} }

View File

@ -76,7 +76,7 @@ public function update(RawMaterialPrice $variant, array $data): RawMaterialPrice
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
title: 'Varian Diperbarui', title: 'Varian Diperbarui',
body: "Varian \"{$variant->variant}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.', body: "Varian \"{$variant->variant}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.',
url: route('admin.master.raw-materials.index'), url: route('admin.master.raw-materials.index', ['highlight' => $variant->raw_material_id]),
); );
return $variant->fresh(); return $variant->fresh();

View File

@ -0,0 +1,80 @@
# Fitur: Notification Highlight — Klik Notifikasi → Tabel Filtered by ID
## Tujuan
Saat user klik notifikasi, mereka dibawa ke halaman tabel yang sesuai (misalnya Transaksi, Kasbon, dll), tapi hanya menampilkan data spesifik dari notifikasi tersebut. User tetap di halaman tabel dan bisa klik "Tampilkan semua" untuk kembali ke semua data.
## File yang Dibaca
### Backend
- `app/Http/Requests/PaginatedRequest.php` — tambah `highlight` ke `validatedWithDefaults()`
- `app/Services/Admin/Master/CategoryService.php` — tambah `$highlight` parameter + `->where('id', $highlight)`
- `app/Services/Admin/Finance/ExpenseService.php` — sama
- `app/Services/Admin/Finance/EmployeeAdvanceService.php` — sama
- `app/Services/Admin/Finance/Payroll/PayrollPeriodService.php` — sama
- `app/Services/Admin/Finance/Cash/CashTransactionService.php` — sama
- `app/Services/Admin/Master/RawMaterial/RawMaterialService.php` — sama
- `app/Services/Admin/Master/Product/ProductService.php` — sama
- `app/Services/Admin/HR/LeaveRequestService.php` — sama
- `app/Services/Admin/Manage/RestockService.php` — sama
- `app/Services/Admin/Manage/PurchaseService.php` — sama
- `app/Services/Admin/Manage/CuttingService.php` — sama
- `app/Services/Admin/Manage/TransactionService.php` — sama
- 13 Controller files — tambah `'highlight' => $request->input('highlight')` ke Inertia::render
- 14 Service files — update notification URL dengan `['highlight' => $entity->id]`
### Frontend
- `resources/js/components/notifications/notification-bell.tsx` — mark as read saat klik
- 13 Index pages — tambah `highlight?: number` prop + info message
## Pola yang Ditemukan
### Backend Flow
1. `PaginatedRequest` validasi `highlight` query parameter
2. `validatedWithDefaults()` return `highlight` ke controller
3. Controller spread ke service: `$this->service->paginated(...$request->validatedWithDefaults())`
4. Service filter: `->when($highlight, fn ($q) => $q->where('id', $highlight))`
5. Controller pass `highlight` ke view: `'highlight' => $request->input('highlight')`
### Frontend Flow
1. Notification URL sudah include `?highlight={id}` (contoh: `/admin/manage/transactions?highlight=123`)
2. Klik notifikasi → `router.visit(notification.url)` + `markAsRead(id)`
3. Halaman index terima `highlight` prop
4. Tampilkan info message: "Menampilkan data dari notifikasi. [Tampilkan semua]"
5. Klik "Tampilkan semua" → navigasi ke index tanpa highlight parameter
### Notification URL Pattern
```php
// Sebelum:
url: route('admin.manage.transactions.index')
// Sesudah:
url: route('admin.manage.transactions.index', ['highlight' => $order->id])
```
## Perubahan yang Dilakukan
### 1. PaginatedRequest (`app/Http/Requests/PaginatedRequest.php`)
- Tambah `'highlight' => $validated['highlight'] ?? null` ke `validatedWithDefaults()`
### 2. Services (14 files)
- Tambah `?int $highlight = null` parameter ke `paginated()` method
- Tambah `->when($highlight, fn ($q) => $q->where('id', $highlight))` di query
### 3. Controllers (13 files)
- Tambah `'highlight' => $request->input('highlight')` ke Inertia::render data
### 4. Notification URLs (48 call sites di 14 service files)
- Update semua `route('...')` menjadi `route('...', ['highlight' => $entity->id])`
### 5. Notification Bell (`notification-bell.tsx`)
- Tambah `markAsRead()` saat klik notifikasi (sebelum navigasi)
### 6. Index Pages (13 files)
- Tambah `highlight?: number` ke Props type
- Tambah `highlight` ke destructured props
- Tambah info message di PageHeader dengan tombol "Tampilkan semua"
## Catatan
- Attendance page menggunakan pattern berbeda (year/month params), jadi "Tampilkan semua" preserve year/month
- Jobs (CheckAttendancePenaltiesJob, SendAttendanceReminderJob) tidak di-update karena tidak ada specific entity ID
- Pre-existing TypeScript errors tidak terkait dengan fitur ini

View File

@ -194,6 +194,9 @@ export function NotificationBell() {
className="min-w-0 flex-1 cursor-pointer" className="min-w-0 flex-1 cursor-pointer"
onClick={() => { onClick={() => {
if (notification.url) { if (notification.url) {
if (!notification.is_read) {
void markAsRead(notification.id);
}
router.visit(notification.url); router.visit(notification.url);
} }
}} }}

View File

@ -62,6 +62,7 @@ type Props = {
filterOptions: { filterOptions: {
typeOptions: TypeOption[]; typeOptions: TypeOption[];
}; };
highlight?: number;
}; };
export default function CashAccountIndex({ export default function CashAccountIndex({
@ -69,6 +70,7 @@ export default function CashAccountIndex({
transactions, transactions,
filters, filters,
filterOptions, filterOptions,
highlight,
}: Props) { }: Props) {
const { can } = useCan(); const { can } = useCan();
const [depositOpen, setDepositOpen] = useState(false); const [depositOpen, setDepositOpen] = useState(false);
@ -181,6 +183,28 @@ export default function CashAccountIndex({
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-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">
<PageHeader <PageHeader
title="Kas Toko" title="Kas Toko"
description={
highlight && (
<p className="mt-1 text-sm text-muted-foreground">
Menampilkan transaksi kas dari notifikasi.
<button
onClick={() => {
router.get(
cashAccountIndex.url(),
{},
{
replace: true,
preserveState: true,
},
);
}}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
Tampilkan semua
</button>
</p>
)
}
actions={ actions={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{can('cash.deposit') && ( {can('cash.deposit') && (

View File

@ -60,12 +60,14 @@ type Props = {
filterOptions: { filterOptions: {
statusOptions: TypeOption[]; statusOptions: TypeOption[];
}; };
highlight?: number;
}; };
export default function EmployeeAdvanceIndex({ export default function EmployeeAdvanceIndex({
employeeAdvances, employeeAdvances,
filters, filters,
filterOptions, filterOptions,
highlight,
}: Props) { }: Props) {
const { can } = useCan(); const { can } = useCan();
const { auth } = usePage().props as { auth: { user: { id: number } } }; const { auth } = usePage().props as { auth: { user: { id: number } } };
@ -181,6 +183,28 @@ export default function EmployeeAdvanceIndex({
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-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">
<PageHeader <PageHeader
title="Kasbon" title="Kasbon"
description={
highlight && (
<p className="mt-1 text-sm text-muted-foreground">
Menampilkan kasbon dari notifikasi.
<button
onClick={() => {
router.get(
employeeAdvanceIndex.url(),
{},
{
replace: true,
preserveState: true,
},
);
}}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
Tampilkan semua
</button>
</p>
)
}
actions={ actions={
can('employee_advances.create') ? ( can('employee_advances.create') ? (
<Button asChild> <Button asChild>

View File

@ -31,9 +31,10 @@ type Props = {
per_page: number; per_page: number;
total: number; total: number;
}; };
highlight?: number;
}; };
export default function ExpenseIndex({ expenses }: Props) { export default function ExpenseIndex({ expenses, highlight }: Props) {
const { can } = useCan(); const { can } = useCan();
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [editing, setEditing] = useState<Expense | null>(null); const [editing, setEditing] = useState<Expense | null>(null);
@ -96,6 +97,28 @@ export default function ExpenseIndex({ expenses }: Props) {
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-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">
<PageHeader <PageHeader
title="Pengeluaran" title="Pengeluaran"
description={
highlight && (
<p className="mt-1 text-sm text-muted-foreground">
Menampilkan pengeluaran dari notifikasi.
<button
onClick={() => {
router.get(
expenseIndex.url(),
{},
{
replace: true,
preserveState: true,
},
);
}}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
Tampilkan semua
</button>
</p>
)
}
actions={ actions={
can('expenses.create') ? ( can('expenses.create') ? (
<Button asChild> <Button asChild>

View File

@ -24,9 +24,10 @@ type Props = {
per_page: number; per_page: number;
total: number; total: number;
}; };
highlight?: number;
}; };
export default function PayrollPeriodIndex({ payrollPeriods }: Props) { export default function PayrollPeriodIndex({ payrollPeriods, highlight }: Props) {
const { can, hasAnyRole } = useCan(); const { can, hasAnyRole } = useCan();
const canViewAll = hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']); const canViewAll = hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
const [closing, setClosing] = useState<PayrollPeriod | null>(null); const [closing, setClosing] = useState<PayrollPeriod | null>(null);
@ -90,7 +91,31 @@ export default function PayrollPeriodIndex({ payrollPeriods }: Props) {
<Head title="Gaji" /> <Head title="Gaji" />
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-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">
<PageHeader title="Gaji" /> <PageHeader
title="Gaji"
description={
highlight && (
<p className="mt-1 text-sm text-muted-foreground">
Menampilkan periode gaji dari notifikasi.
<button
onClick={() => {
router.get(
payrollPeriodsIndex.url(),
{},
{
replace: true,
preserveState: true,
},
);
}}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
Tampilkan semua
</button>
</p>
)
}
/>
<DataTable <DataTable
columns={columns} columns={columns}

View File

@ -72,6 +72,7 @@ type Props = {
isOnLeave: boolean; isOnLeave: boolean;
canCheckIn: boolean; canCheckIn: boolean;
isAdmin: boolean; isAdmin: boolean;
highlight?: number;
}; };
const WEEKDAYS = ['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab']; const WEEKDAYS = ['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab'];
@ -156,6 +157,7 @@ export default function AttendanceIndex({
isOnLeave, isOnLeave,
canCheckIn, canCheckIn,
isAdmin, isAdmin,
highlight,
}: Props) { }: Props) {
const [officeHour, officeMinute] = hrSettings.scheduled_check_in_time const [officeHour, officeMinute] = hrSettings.scheduled_check_in_time
.split(':') .split(':')
@ -367,6 +369,29 @@ export default function AttendanceIndex({
<h2 className="text-2xl font-semibold tracking-tight"> <h2 className="text-2xl font-semibold tracking-tight">
Presensi Presensi
</h2> </h2>
{highlight && (
<p className="mt-1 text-sm text-muted-foreground">
Menampilkan presensi dari notifikasi.
<button
onClick={() => {
router.get(
attendanceIndex.url(),
{
year: currentYear,
month: currentMonth,
},
{
replace: true,
preserveState: true,
},
);
}}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
Tampilkan semua
</button>
</p>
)}
</div> </div>
{!isAdmin && ( {!isAdmin && (

View File

@ -51,6 +51,7 @@ type Props = {
statusOptions: StatusOption[]; statusOptions: StatusOption[];
}; };
currentUserId: number; currentUserId: number;
highlight?: number;
}; };
function toLocalDateString(date: Date): string { function toLocalDateString(date: Date): string {
@ -60,7 +61,7 @@ function toLocalDateString(date: Date): string {
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
} }
export default function LeaveRequestIndex({ leaveRequests, filters, filterOptions, currentUserId }: Props) { export default function LeaveRequestIndex({ leaveRequests, filters, filterOptions, currentUserId, highlight }: Props) {
const { can } = useCan(); const { can } = useCan();
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [editing, setEditing] = useState<LeaveRequest | null>(null); const [editing, setEditing] = useState<LeaveRequest | null>(null);
@ -192,6 +193,28 @@ export default function LeaveRequestIndex({ leaveRequests, filters, filterOption
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-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">
<PageHeader <PageHeader
title="Cuti" title="Cuti"
description={
highlight && (
<p className="mt-1 text-sm text-muted-foreground">
Menampilkan cuti dari notifikasi.
<button
onClick={() => {
router.get(
leaveRequestIndex.url(),
{},
{
replace: true,
preserveState: true,
},
);
}}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
Tampilkan semua
</button>
</p>
)
}
actions={ actions={
can('leave_requests.create') ? ( can('leave_requests.create') ? (
<Button asChild> <Button asChild>

View File

@ -52,12 +52,14 @@ type Props = {
statusOptions: Array<{ value: string; label: string }>; statusOptions: Array<{ value: string; label: string }>;
productNames: Array<{ product_name: string }>; productNames: Array<{ product_name: string }>;
}; };
highlight?: number;
}; };
export default function CuttingIndex({ export default function CuttingIndex({
cuttings, cuttings,
filters, filters,
filterOptions, filterOptions,
highlight,
}: Props) { }: Props) {
const { can } = useCan(); const { can } = useCan();
const [deleting, setDeleting] = useState<Cutting | null>(null); const [deleting, setDeleting] = useState<Cutting | null>(null);
@ -221,6 +223,28 @@ export default function CuttingIndex({
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-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">
<PageHeader <PageHeader
title="Cutting" title="Cutting"
description={
highlight && (
<p className="mt-1 text-sm text-muted-foreground">
Menampilkan cutting dari notifikasi.
<button
onClick={() => {
router.get(
cuttingIndex.url(),
{},
{
replace: true,
preserveState: true,
},
);
}}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
Tampilkan semua
</button>
</p>
)
}
actions={ actions={
can('cuttings.create') ? ( can('cuttings.create') ? (
<Button asChild> <Button asChild>

View File

@ -43,12 +43,14 @@ type Props = {
filters: { filters: {
supplier_id?: string; supplier_id?: string;
}; };
highlight?: number;
}; };
export default function PurchaseIndex({ export default function PurchaseIndex({
purchases, purchases,
filters, filters,
suppliers, suppliers,
highlight,
}: Props) { }: Props) {
const { can } = useCan(); const { can } = useCan();
const [deleting, setDeleting] = useState<Purchase | null>(null); const [deleting, setDeleting] = useState<Purchase | null>(null);
@ -171,6 +173,28 @@ export default function PurchaseIndex({
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-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">
<PageHeader <PageHeader
title="Belanja" title="Belanja"
description={
highlight && (
<p className="mt-1 text-sm text-muted-foreground">
Menampilkan belanja dari notifikasi.
<button
onClick={() => {
router.get(
purchaseIndex.url(),
{},
{
replace: true,
preserveState: true,
},
);
}}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
Tampilkan semua
</button>
</p>
)
}
actions={ actions={
can('purchases.create') ? ( can('purchases.create') ? (
<Button asChild> <Button asChild>

View File

@ -27,9 +27,10 @@ type Props = {
per_page: number; per_page: number;
total: number; total: number;
}; };
highlight?: number;
}; };
export default function RestockIndex({ restocks }: Props) { export default function RestockIndex({ restocks, highlight }: Props) {
const { can } = useCan(); const { can } = useCan();
const [deleting, setDeleting] = useState<Restock | null>(null); const [deleting, setDeleting] = useState<Restock | null>(null);
const [loadedItems, setLoadedItems] = useState<Record<number, RestockItem[]>>({}); const [loadedItems, setLoadedItems] = useState<Record<number, RestockItem[]>>({});
@ -93,6 +94,28 @@ export default function RestockIndex({ restocks }: Props) {
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-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">
<PageHeader <PageHeader
title="Restock" title="Restock"
description={
highlight && (
<p className="mt-1 text-sm text-muted-foreground">
Menampilkan restock dari notifikasi.
<button
onClick={() => {
router.get(
restockIndex.url(),
{},
{
replace: true,
preserveState: true,
},
);
}}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
Tampilkan semua
</button>
</p>
)
}
actions={ actions={
can('restocks.create') ? ( can('restocks.create') ? (
<Button asChild> <Button asChild>

View File

@ -90,6 +90,7 @@ type Props = {
customers: FilterOption[]; customers: FilterOption[];
employees: FilterOption[]; employees: FilterOption[];
}; };
highlight?: number;
}; };
export default function TransactionIndex({ export default function TransactionIndex({
@ -97,6 +98,7 @@ export default function TransactionIndex({
summary, summary,
filters, filters,
filterOptions, filterOptions,
highlight,
}: Props) { }: Props) {
const { can } = useCan(); const { can } = useCan();
const { name: appName, address: appAddress } = usePage().props as unknown as { name: string; address: string }; const { name: appName, address: appAddress } = usePage().props as unknown as { name: string; address: string };
@ -459,6 +461,28 @@ export default function TransactionIndex({
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-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">
<PageHeader <PageHeader
title="Transaksi" title="Transaksi"
description={
highlight && (
<p className="mt-1 text-sm text-muted-foreground">
Menampilkan transaksi dari notifikasi.
<button
onClick={() => {
router.get(
transactionIndex.url(),
{},
{
replace: true,
preserveState: true,
},
);
}}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
Tampilkan semua
</button>
</p>
)
}
actions={ actions={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{printer.connected ? ( {printer.connected ? (

View File

@ -29,10 +29,9 @@ type Props = {
per_page: number; per_page: number;
total: number; total: number;
}; };
highlight?: number;
}; };
export default function CategoryIndex({ categories, highlight }: Props) { export default function CategoryIndex({ categories }: Props) {
const { can } = useCan(); const { can } = useCan();
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [editing, setEditing] = useState<Category | null>(null); const [editing, setEditing] = useState<Category | null>(null);
@ -78,28 +77,6 @@ export default function CategoryIndex({ categories, highlight }: Props) {
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-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">
<PageHeader <PageHeader
title="Kategori" title="Kategori"
description={
highlight && (
<p className="mt-1 text-sm text-muted-foreground">
Menampilkan kategori dari notifikasi.
<button
onClick={() => {
router.get(
categoryIndex.url(),
{},
{
replace: true,
preserveState: true,
},
);
}}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
Tampilkan semua
</button>
</p>
)
}
actions={ actions={
can('categories.create') ? ( can('categories.create') ? (
<Button asChild> <Button asChild>

View File

@ -67,9 +67,10 @@ type Props = {
category?: string; category?: string;
featured?: string; featured?: string;
}; };
highlight?: number;
}; };
export default function ProductIndex({ products, categories, productNames, filters }: Props) { export default function ProductIndex({ products, categories, productNames, filters, highlight }: Props) {
const { can } = useCan(); const { can } = useCan();
const [deleting, setDeleting] = useState<Product | null>(null); const [deleting, setDeleting] = useState<Product | null>(null);
const [deletingVariant, setDeletingVariant] = useState<{ const [deletingVariant, setDeletingVariant] = useState<{
@ -323,6 +324,28 @@ export default function ProductIndex({ products, categories, productNames, filte
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-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">
<PageHeader <PageHeader
title="Produk" title="Produk"
description={
highlight && (
<p className="mt-1 text-sm text-muted-foreground">
Menampilkan produk dari notifikasi.
<button
onClick={() => {
router.get(
productIndex.url(),
{},
{
replace: true,
preserveState: true,
},
);
}}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
Tampilkan semua
</button>
</p>
)
}
actions={ actions={
can('products.create') ? ( can('products.create') ? (
<Button asChild> <Button asChild>

View File

@ -54,9 +54,10 @@ type Props = {
name?: string; name?: string;
stock?: string; stock?: string;
}; };
highlight?: number;
}; };
export default function RawMaterialIndex({ rawMaterials, rawMaterialNames, filters }: Props) { export default function RawMaterialIndex({ rawMaterials, rawMaterialNames, filters, highlight }: Props) {
const { can } = useCan(); const { can } = useCan();
const [deleting, setDeleting] = useState<RawMaterial | null>(null); const [deleting, setDeleting] = useState<RawMaterial | null>(null);
const [deletingVariant, setDeletingVariant] = useState<{ const [deletingVariant, setDeletingVariant] = useState<{
@ -240,6 +241,28 @@ export default function RawMaterialIndex({ rawMaterials, rawMaterialNames, filte
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-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">
<PageHeader <PageHeader
title="Bahan Baku" title="Bahan Baku"
description={
highlight && (
<p className="mt-1 text-sm text-muted-foreground">
Menampilkan bahan baku dari notifikasi.
<button
onClick={() => {
router.get(
rawMaterialIndex.url(),
{},
{
replace: true,
preserveState: true,
},
);
}}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
Tampilkan semua
</button>
</p>
)
}
actions={ actions={
can('raw_materials.create') ? ( can('raw_materials.create') ? (
<Link href={rawMaterialCreate.url()}> <Link href={rawMaterialCreate.url()}>