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:
parent
aff7ddbcf2
commit
22c9a4cf2a
@ -32,6 +32,7 @@ public function index(PaginatedRequest $request): Response
|
||||
'filterOptions' => [
|
||||
'typeOptions' => CashTransactionType::toSelect(),
|
||||
],
|
||||
'highlight' => $request->input('highlight'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -30,6 +30,7 @@ public function index(PaginatedRequest $request): Response
|
||||
'filterOptions' => [
|
||||
'statusOptions' => EmployeeAdvanceStatus::toSelect(),
|
||||
],
|
||||
'highlight' => $request->input('highlight'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/finance/expense/index', [
|
||||
'expenses' => $this->service->paginated(...$request->validatedWithDefaults()),
|
||||
'highlight' => $request->input('highlight'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/finance/payroll-period/index', [
|
||||
'payrollPeriods' => $this->service->paginated(...$request->validatedWithDefaults()),
|
||||
'highlight' => $request->input('highlight'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ public function index(Request $request): Response
|
||||
|
||||
return Inertia::render('admin/hr/attendance/index', [
|
||||
...$this->service->getIndexData($year, $month),
|
||||
'highlight' => $request->input('highlight'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -30,6 +30,7 @@ public function index(PaginatedRequest $request): Response
|
||||
'statusOptions' => LeaveRequestStatus::toSelect(),
|
||||
],
|
||||
'currentUserId' => $request->user()->id,
|
||||
'highlight' => $request->input('highlight'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ public function index(PaginatedRequest $request): Response
|
||||
'statusOptions' => CuttingStatus::toSelect(),
|
||||
'productNames' => $this->service->getProductNames(),
|
||||
],
|
||||
'highlight' => $request->input('highlight'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -31,6 +31,7 @@ public function index(PaginatedRequest $request): Response
|
||||
),
|
||||
'suppliers' => $this->supplierService->getAll(),
|
||||
'filters' => $request->only(['supplier_id']),
|
||||
'highlight' => $request->input('highlight'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ public function index(PaginatedRequest $request): Response
|
||||
'restocks' => $this->service->paginated(
|
||||
...$request->validatedWithDefaults(),
|
||||
),
|
||||
'highlight' => $request->input('highlight'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ public function index(PaginatedRequest $request): Response
|
||||
'summary' => $this->service->getSummary($filters, $user),
|
||||
'filters' => $filters,
|
||||
'filterOptions' => $this->service->getFilterOptions(),
|
||||
'highlight' => $request->input('highlight'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -30,6 +30,7 @@ public function index(PaginatedRequest $request): Response
|
||||
'categories' => $this->categoryService->getAll(),
|
||||
'productNames' => $this->service->getNames(),
|
||||
'filters' => $request->only(['status', 'stock', 'category', 'name', 'featured']),
|
||||
'highlight' => $request->input('highlight'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ public function index(PaginatedRequest $request): Response
|
||||
),
|
||||
'rawMaterialNames' => $this->service->getNames(),
|
||||
'filters' => $request->only(['is_active', 'stock', 'name']),
|
||||
'highlight' => $request->input('highlight'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -26,11 +26,17 @@ public function validatedWithDefaults(): array
|
||||
{
|
||||
$validated = $this->validated();
|
||||
|
||||
return [
|
||||
$result = [
|
||||
'perPage' => $validated['per_page'] ?? 25,
|
||||
'search' => $validated['search'] ?? '',
|
||||
'sort' => $validated['sort'] ?? 'created_at',
|
||||
'direction' => $validated['direction'] ?? 'desc',
|
||||
];
|
||||
|
||||
if (! empty($validated['highlight'])) {
|
||||
$result['highlight'] = (int) $validated['highlight'];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ public function __construct(
|
||||
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();
|
||||
|
||||
@ -36,6 +36,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
$paginator = $cashAccount->cashTransactions()
|
||||
->select(['id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at'])
|
||||
->with('createdBy.userProfile', 'media')
|
||||
->when($highlight, fn ($q) => $q->where('id', $highlight))
|
||||
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
||||
->when($filters['type'] ?? null, function ($query, $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],
|
||||
title: 'Setoran Kas Toko',
|
||||
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;
|
||||
@ -85,7 +86,7 @@ public function withdrawal(array $data): CashTransaction
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Penarikan Kas Toko',
|
||||
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;
|
||||
@ -133,7 +134,7 @@ public function update(CashTransaction $transaction, array $data): CashTransacti
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Transaksi Kas Diperbarui',
|
||||
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;
|
||||
|
||||
@ -18,11 +18,12 @@ class EmployeeAdvanceService
|
||||
{
|
||||
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()
|
||||
->select(['id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at'])
|
||||
->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($filters['status'] ?? null, fn ($q) => $q->where('status', $filters['status']))
|
||||
->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],
|
||||
title: 'Kasbon Baru',
|
||||
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,
|
||||
);
|
||||
|
||||
@ -171,7 +172,7 @@ public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Kasbon Dikeluarkan',
|
||||
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,
|
||||
);
|
||||
|
||||
@ -224,7 +225,7 @@ public function pay(EmployeeAdvance $employeeAdvance, int $amount): EmployeeAdva
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: $employeeAdvance->status === EmployeeAdvanceStatus::REPAID ? 'Kasbon Dikembalikan' : 'Pembayaran Kasbon',
|
||||
body: $notificationBody,
|
||||
url: route('admin.finance.employee-advances.index'),
|
||||
url: route('admin.finance.employee-advances.index', ['highlight' => $employeeAdvance->id]),
|
||||
additionalUser: $employeeAdvance->employee->user ?? null,
|
||||
);
|
||||
|
||||
|
||||
@ -23,11 +23,12 @@ public function __construct(
|
||||
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()
|
||||
->select(['id', 'created_by_id', 'amount', 'description', 'created_at'])
|
||||
->with('createdBy.userProfile', 'media')
|
||||
->when($highlight, fn ($q) => $q->where('id', $highlight))
|
||||
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
||||
->orderBy($sort, $direction)
|
||||
->paginate($perPage);
|
||||
@ -63,7 +64,7 @@ public function store(array $data): Expense
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Pengeluaran Baru',
|
||||
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;
|
||||
@ -110,7 +111,7 @@ public function update(Expense $expense, array $data): Expense
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Pengeluaran Diperbarui',
|
||||
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;
|
||||
|
||||
@ -19,10 +19,11 @@ class PayrollPeriodService
|
||||
{
|
||||
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()
|
||||
->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) {
|
||||
$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')
|
||||
@ -148,7 +149,7 @@ public function pay(Payroll $payroll): Payroll
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Gaji Dibayar',
|
||||
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,
|
||||
);
|
||||
|
||||
@ -179,7 +180,7 @@ public function cancel(Payroll $payroll): Payroll
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Gaji Dibatalkan',
|
||||
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,
|
||||
);
|
||||
|
||||
|
||||
@ -175,7 +175,7 @@ public function checkIn(array $data): Attendance
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
|
||||
title: 'Presensi Masuk',
|
||||
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;
|
||||
@ -197,7 +197,7 @@ public function checkOut(Attendance $attendance, array $data): Attendance
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
|
||||
title: 'Presensi Pulang',
|
||||
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;
|
||||
|
||||
@ -16,11 +16,12 @@ class LeaveRequestService
|
||||
{
|
||||
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()
|
||||
->select(['id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at'])
|
||||
->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($search, fn ($q) => $q->whereHas('employee.user.userProfile', fn ($uq) => $uq->where('full_name', 'like', "%{$search}%")))
|
||||
->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],
|
||||
title: 'Pengajuan Cuti Baru',
|
||||
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,
|
||||
);
|
||||
|
||||
@ -115,7 +116,7 @@ public function approve(LeaveRequest $leaveRequest): LeaveRequest
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Cuti Disetujui',
|
||||
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,
|
||||
);
|
||||
|
||||
@ -136,7 +137,7 @@ public function reject(LeaveRequest $leaveRequest): LeaveRequest
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Cuti Ditolak',
|
||||
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,
|
||||
);
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ public function __construct(
|
||||
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)';
|
||||
$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("{$productNameQuery} as product_name")
|
||||
->selectRaw("{$cuttingResultQuery} as cutting_result")
|
||||
->when($highlight, fn ($q) => $q->where('id', $highlight))
|
||||
->when($search, function ($q) use ($search) {
|
||||
$q->whereHas('cuttingResults', fn ($rq) => $rq->where('product_name', '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],
|
||||
title: 'Cutting Baru',
|
||||
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;
|
||||
@ -492,7 +493,7 @@ public function update(Cutting $cutting, array $data): Cutting
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
|
||||
title: 'Cutting Diperbarui',
|
||||
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;
|
||||
@ -538,7 +539,7 @@ public function complete(Cutting $cutting): Cutting
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
|
||||
title: 'Cutting Selesai',
|
||||
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;
|
||||
|
||||
@ -23,7 +23,7 @@ public function __construct(
|
||||
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)';
|
||||
$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("{$materialNameQuery} as material_name")
|
||||
->selectRaw("{$unitQuery} as unit")
|
||||
->when($highlight, fn ($q) => $q->where('id', $highlight))
|
||||
->when($search, function ($q) use ($search) {
|
||||
$q->whereHas('supplier', fn ($sq) => $sq->where('name', '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],
|
||||
title: 'Belanja Baru',
|
||||
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;
|
||||
@ -335,7 +336,7 @@ private function storeNew(array $data): Purchase
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
|
||||
title: 'Belanja Baru',
|
||||
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;
|
||||
@ -482,7 +483,7 @@ public function update(Purchase $purchase, array $data): Purchase
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
|
||||
title: 'Belanja Diperbarui',
|
||||
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;
|
||||
|
||||
@ -23,7 +23,7 @@ public function __construct(
|
||||
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)';
|
||||
$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("{$totalQtyQuery} as total_qty")
|
||||
->selectRaw("{$productNamesQuery} as product_names")
|
||||
->when($highlight, fn ($q) => $q->where('id', $highlight))
|
||||
->when($search, function ($q) use ($search) {
|
||||
$q->whereHas('restockItems.productVariant.product', fn ($sq) => $sq->where('name', 'like', "%{$search}%"))
|
||||
->orWhere('notes', 'like', "%{$search}%");
|
||||
@ -98,7 +99,7 @@ public function store(array $data): Restock
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
|
||||
title: 'Restock Baru',
|
||||
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;
|
||||
@ -143,7 +144,7 @@ public function update(Restock $restock, array $data): Restock
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
|
||||
title: 'Restock Diperbarui',
|
||||
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;
|
||||
|
||||
@ -39,7 +39,7 @@ public function __construct(
|
||||
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)';
|
||||
$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("{$totalQtyQuery} as total_qty")
|
||||
->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($search, function ($q) use ($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],
|
||||
title: 'Transaksi Baru',
|
||||
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;
|
||||
@ -339,7 +340,7 @@ public function update(Order $order, array $data): Order
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
|
||||
title: 'Transaksi Diperbarui',
|
||||
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;
|
||||
|
||||
@ -32,7 +32,7 @@ public function getNames(): Collection
|
||||
->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)';
|
||||
$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("{$rejectSumQuery} as total_reject_stock")
|
||||
->selectRaw("{$retailSumQuery} as total_retail_stock")
|
||||
->when($highlight, fn ($q) => $q->where('id', $highlight))
|
||||
->when($search, fn ($q) => $q->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],
|
||||
title: 'Produk Baru',
|
||||
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;
|
||||
@ -401,7 +402,7 @@ public function update(Product $product, array $data): Product
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Produk Diperbarui',
|
||||
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;
|
||||
@ -446,7 +447,7 @@ public function toggleStatus(Product $product): void
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Status Produk Diubah',
|
||||
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],
|
||||
title: 'Status Unggulan Produk Diubah',
|
||||
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],
|
||||
title: 'Produk Disetujui',
|
||||
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,
|
||||
);
|
||||
}
|
||||
@ -494,7 +495,7 @@ public function reject(Product $product, string $reason = ''): void
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
|
||||
title: 'Produk Ditolak',
|
||||
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,
|
||||
);
|
||||
}
|
||||
@ -510,7 +511,7 @@ public function resubmit(Product $product): void
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
|
||||
title: 'Produk Diajukan Ulang',
|
||||
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]),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -144,7 +144,7 @@ public function update(ProductVariant $variant, array $data): ProductVariant
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Varian Diperbarui',
|
||||
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();
|
||||
@ -206,7 +206,7 @@ public function transferStock(ProductVariant $variant, array $data): ProductVari
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Transfer Stok',
|
||||
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();
|
||||
|
||||
@ -27,7 +27,7 @@ public function getNames(): Collection
|
||||
->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)';
|
||||
$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')
|
||||
->selectRaw("{$stockSumQuery} as total_stock")
|
||||
->selectRaw("{$valueSumQuery} as total_value")
|
||||
->when($highlight, fn ($q) => $q->where('id', $highlight))
|
||||
->when($search, fn ($q) => $q->where('name', '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],
|
||||
title: 'Bahan Baku Baru',
|
||||
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;
|
||||
@ -230,7 +231,7 @@ public function update(RawMaterial $rawMaterial, array $data): RawMaterial
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Bahan Baku Diperbarui',
|
||||
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;
|
||||
@ -268,7 +269,7 @@ public function toggleStatus(RawMaterial $rawMaterial): void
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Status Bahan Baku Diubah',
|
||||
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]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ public function update(RawMaterialPrice $variant, array $data): RawMaterialPrice
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Varian Diperbarui',
|
||||
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();
|
||||
|
||||
80
docs/notification-highlight.md
Normal file
80
docs/notification-highlight.md
Normal 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
|
||||
@ -194,6 +194,9 @@ export function NotificationBell() {
|
||||
className="min-w-0 flex-1 cursor-pointer"
|
||||
onClick={() => {
|
||||
if (notification.url) {
|
||||
if (!notification.is_read) {
|
||||
void markAsRead(notification.id);
|
||||
}
|
||||
router.visit(notification.url);
|
||||
}
|
||||
}}
|
||||
|
||||
@ -62,6 +62,7 @@ type Props = {
|
||||
filterOptions: {
|
||||
typeOptions: TypeOption[];
|
||||
};
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
export default function CashAccountIndex({
|
||||
@ -69,6 +70,7 @@ export default function CashAccountIndex({
|
||||
transactions,
|
||||
filters,
|
||||
filterOptions,
|
||||
highlight,
|
||||
}: Props) {
|
||||
const { can } = useCan();
|
||||
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">
|
||||
<PageHeader
|
||||
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={
|
||||
<div className="flex items-center gap-2">
|
||||
{can('cash.deposit') && (
|
||||
|
||||
@ -60,12 +60,14 @@ type Props = {
|
||||
filterOptions: {
|
||||
statusOptions: TypeOption[];
|
||||
};
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
export default function EmployeeAdvanceIndex({
|
||||
employeeAdvances,
|
||||
filters,
|
||||
filterOptions,
|
||||
highlight,
|
||||
}: Props) {
|
||||
const { can } = useCan();
|
||||
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">
|
||||
<PageHeader
|
||||
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={
|
||||
can('employee_advances.create') ? (
|
||||
<Button asChild>
|
||||
|
||||
@ -31,9 +31,10 @@ type Props = {
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
export default function ExpenseIndex({ expenses }: Props) {
|
||||
export default function ExpenseIndex({ expenses, highlight }: Props) {
|
||||
const { can } = useCan();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
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">
|
||||
<PageHeader
|
||||
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={
|
||||
can('expenses.create') ? (
|
||||
<Button asChild>
|
||||
|
||||
@ -24,9 +24,10 @@ type Props = {
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
export default function PayrollPeriodIndex({ payrollPeriods }: Props) {
|
||||
export default function PayrollPeriodIndex({ payrollPeriods, highlight }: Props) {
|
||||
const { can, hasAnyRole } = useCan();
|
||||
const canViewAll = hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
|
||||
const [closing, setClosing] = useState<PayrollPeriod | null>(null);
|
||||
@ -90,7 +91,31 @@ export default function PayrollPeriodIndex({ payrollPeriods }: Props) {
|
||||
<Head title="Gaji" />
|
||||
|
||||
<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
|
||||
columns={columns}
|
||||
|
||||
@ -72,6 +72,7 @@ type Props = {
|
||||
isOnLeave: boolean;
|
||||
canCheckIn: boolean;
|
||||
isAdmin: boolean;
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
const WEEKDAYS = ['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab'];
|
||||
@ -156,6 +157,7 @@ export default function AttendanceIndex({
|
||||
isOnLeave,
|
||||
canCheckIn,
|
||||
isAdmin,
|
||||
highlight,
|
||||
}: Props) {
|
||||
const [officeHour, officeMinute] = hrSettings.scheduled_check_in_time
|
||||
.split(':')
|
||||
@ -367,6 +369,29 @@ export default function AttendanceIndex({
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Presensi
|
||||
</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>
|
||||
|
||||
{!isAdmin && (
|
||||
|
||||
@ -51,6 +51,7 @@ type Props = {
|
||||
statusOptions: StatusOption[];
|
||||
};
|
||||
currentUserId: number;
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
function toLocalDateString(date: Date): string {
|
||||
@ -60,7 +61,7 @@ function toLocalDateString(date: Date): string {
|
||||
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 [createOpen, setCreateOpen] = useState(false);
|
||||
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">
|
||||
<PageHeader
|
||||
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={
|
||||
can('leave_requests.create') ? (
|
||||
<Button asChild>
|
||||
|
||||
@ -52,12 +52,14 @@ type Props = {
|
||||
statusOptions: Array<{ value: string; label: string }>;
|
||||
productNames: Array<{ product_name: string }>;
|
||||
};
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
export default function CuttingIndex({
|
||||
cuttings,
|
||||
filters,
|
||||
filterOptions,
|
||||
highlight,
|
||||
}: Props) {
|
||||
const { can } = useCan();
|
||||
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">
|
||||
<PageHeader
|
||||
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={
|
||||
can('cuttings.create') ? (
|
||||
<Button asChild>
|
||||
|
||||
@ -43,12 +43,14 @@ type Props = {
|
||||
filters: {
|
||||
supplier_id?: string;
|
||||
};
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
export default function PurchaseIndex({
|
||||
purchases,
|
||||
filters,
|
||||
suppliers,
|
||||
highlight,
|
||||
}: Props) {
|
||||
const { can } = useCan();
|
||||
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">
|
||||
<PageHeader
|
||||
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={
|
||||
can('purchases.create') ? (
|
||||
<Button asChild>
|
||||
|
||||
@ -27,9 +27,10 @@ type Props = {
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
export default function RestockIndex({ restocks }: Props) {
|
||||
export default function RestockIndex({ restocks, highlight }: Props) {
|
||||
const { can } = useCan();
|
||||
const [deleting, setDeleting] = useState<Restock | null>(null);
|
||||
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">
|
||||
<PageHeader
|
||||
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={
|
||||
can('restocks.create') ? (
|
||||
<Button asChild>
|
||||
|
||||
@ -90,6 +90,7 @@ type Props = {
|
||||
customers: FilterOption[];
|
||||
employees: FilterOption[];
|
||||
};
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
export default function TransactionIndex({
|
||||
@ -97,6 +98,7 @@ export default function TransactionIndex({
|
||||
summary,
|
||||
filters,
|
||||
filterOptions,
|
||||
highlight,
|
||||
}: Props) {
|
||||
const { can } = useCan();
|
||||
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">
|
||||
<PageHeader
|
||||
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={
|
||||
<div className="flex items-center gap-2">
|
||||
{printer.connected ? (
|
||||
|
||||
@ -29,10 +29,9 @@ type Props = {
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
export default function CategoryIndex({ categories, highlight }: Props) {
|
||||
export default function CategoryIndex({ categories }: Props) {
|
||||
const { can } = useCan();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
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">
|
||||
<PageHeader
|
||||
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={
|
||||
can('categories.create') ? (
|
||||
<Button asChild>
|
||||
|
||||
@ -67,9 +67,10 @@ type Props = {
|
||||
category?: 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 [deleting, setDeleting] = useState<Product | null>(null);
|
||||
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">
|
||||
<PageHeader
|
||||
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={
|
||||
can('products.create') ? (
|
||||
<Button asChild>
|
||||
|
||||
@ -54,9 +54,10 @@ type Props = {
|
||||
name?: 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 [deleting, setDeleting] = useState<RawMaterial | null>(null);
|
||||
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">
|
||||
<PageHeader
|
||||
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={
|
||||
can('raw_materials.create') ? (
|
||||
<Link href={rawMaterialCreate.url()}>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user