Compare commits

..

No commits in common. "138eb8b053ec9c335fa0f5610b5b295d355f9173" and "78dcc4b39dfd66f1958f599b047dee6b7ee96510" have entirely different histories.

38 changed files with 808 additions and 1779 deletions

289
README.md
View File

@ -1,289 +0,0 @@
# DST Collection - Built-in One
Aplikasi manajemen toko dan konveksi untuk DST Collection. Mengelola proses produksi garmen (cutting, jahit, finishing), penjualan di toko, stok bahan baku, pesanan pelanggan, dan laporan keuangan.
## Tech Stack
### Backend
- **PHP** 8.3+
- **Laravel** 13
- **MySQL** 8.0+ (database)
- **Redis** (cache, queue, session)
- **Inertia.js** v3 (server-side rendering)
### Frontend
- **Vue.js** 3.5
- **TypeScript** 5.2
- **Tailwind CSS** 4.1
- **Vite** 7.3 (build tool)
- **Inertia.js** v3 (client-side)
### Packages Utama
- **Spatie** - Permission, Media Library, Activity Log, Settings, Sluggable
- **FullCalendar** - Kalender & penjadwalan
- **TanStack Table** - Tabel data
- **Reka UI** - Komponen UI
- **Lucide** - Ikon
- **Vue Sonner** - Notifikasi toast
- **PWA** - Progressive Web App support
### Development & Testing
- **Pest** v4 (testing)
- **Laravel Pint** (code style)
- **ESLint** (linting)
- **Prettier** (formatting)
- **Vue TSC** (type checking)
## Persyaratan Sistem
| Komponen | Versi Minimum |
|----------|---------------|
| PHP | 8.3 |
| Composer | 2.x |
| Node.js | 18.x |
| NPM | 9.x |
| MySQL | 8.0+ |
| Redis | 6.0+ |
### Ekstensi PHP yang Dibutuhkan
- openssl, pdo, mbstring, tokenizer, xml, ctype, json, bcmath, curl, fileinfo, gd, zip, redis
## Instalasi
### 1. Clone Repository
```bash
git clone <repository-url>
cd built-in-one
```
### 2. Install Dependencies
```bash
composer install
npm install
```
### 3. Setup Environment
```bash
cp .env.example .env
php artisan key:generate
```
### 4. Konfigurasi Database
Edit file `.env`:
**MySQL:**
```env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=dst_collection
DB_USERNAME=root
DB_PASSWORD=
```
### 5. Konfigurasi Redis
Tambahkan di `.env`:
```env
REDIS_CLIENT=predis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
CACHE_STORE=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
```
### 6. Migrate Database
```bash
php artisan migrate
```
### 7. Seed Database (opsional)
```bash
php artisan db:seed
```
### 8. Build Frontend
```bash
npm run build
```
### 9. Storage Link
```bash
php artisan storage:link
```
## Instalasi Satu Perintah
```bash
composer setup
```
Perintah ini akan menjalankan semua langkah instalasi secara otomatis.
## Konfigurasi Tambahan
### Redis
Sistem menggunakan Redis untuk cache, queue, dan session. Pastikan Redis server sudah berjalan:
```bash
redis-cli ping # Harus merespons PONG
```
### Queue Worker
Sistem menggunakan queue untuk beberapa proses. Jalankan queue worker:
```bash
php artisan queue:listen --tries=1 --timeout=0
```
### Push Notification (Web Push)
Untuk mengaktifkan push notification, tambahkan di `.env`:
```env
VAPID_PUBLIC_KEY=<your-vapid-public-key>
VAPID_PRIVATE_KEY=<your-vapid-private-key>
```
Generate VAPID keys:
```bash
php artisan web-push:vapid
```
### Mail Configuration
```env
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
```
## Perintah yang Tersedia
### Development
```bash
composer dev
```
Menjalankan server, queue worker, log viewer, dan Vite secara bersamaan.
### Build
```bash
npm run build # Production build
npm run dev # Development server (Vite)
npm run build:ssr # Build dengan SSR
```
### Code Quality
```bash
composer lint # Auto-fix code style (Pint)
composer lint:check # Check code style
npm run lint # Auto-fix ESLint
npm run lint:check # Check ESLint
npm run format # Format dengan Prettier
npm run format:check # Check formatting
npm run types:check # Type checking (Vue TSC)
```
### Testing
```bash
composer test # Run semua test
php artisan test # Run test langsung
```
### Lainnya
```bash
php artisan migrate # Jalankan migration
php artisan migrate:fresh # Reset dan jalankan ulang migration
php artisan db:seed # Seed database
php artisan storage:link # Buat symlink storage
php artisan config:clear # Clear config cache
php artisan cache:clear # Clear cache
php artisan view:clear # Clear compiled views
```
## Struktur Project
```
built-in-one/
├── app/
│ ├── Http/
│ │ ├── Controllers/ # Controller HTTP
│ │ ├── Middleware/ # Middleware
│ │ └── Requests/ # Form Request validation
│ ├── Models/ # Eloquent Models
│ ├── Services/ # Business Logic
│ └── ...
├── database/
│ ├── factories/ # Model Factories
│ ├── migrations/ # Database Migrations
│ └── seeders/ # Database Seeders
├── resources/
│ ├── js/
│ │ ├── components/ # Vue Components
│ │ ├── pages/ # Inertia Pages
│ │ ├── composables/ # Vue Composables
│ │ ├── types/ # TypeScript Types
│ │ └── constants/ # Constants & Config
│ └── css/ # Stylesheets
├── routes/
│ ├── web.php # Web Routes
│ └── ...
├── tests/
│ ├── Feature/ # Feature Tests
│ └── Unit/ # Unit Tests
├── public/ # Public Assets
├── storage/ # Storage & Logs
├── .env.example # Environment Example
├── composer.json # PHP Dependencies
├── package.json # Node Dependencies
└── vite.config.ts # Vite Configuration
```
## Fitur Utama
### Konveksi
- **Manajemen Cutting** - Kelola proses cutting bahan baku
- **Manajemen Produksi** - Monitoring proses jahit dan finishing
- **Manajemen Stok Bahan Baku** - Monitoring stok kain dan bahan
### Toko
- **Point of Sale (POS)** - Kasir dan penjualan di toko
- **Manajemen Produk** - Kelola katalog produk dan varian
- **Manajemen Pesanan** - Kelola pesanan pelanggan
### Umum
- **Laporan Keuangan** - Laporan pendapatan dan pengeluaran
- **Manajemen Pengguna** - Role dan permission management
- **Activity Log** - Log aktivitas sistem
- **Push Notification** - Notifikasi real-time
- **PWA** - Install sebagai aplikasi mobile
## License
Proprietary - DST Collection

View File

@ -17,14 +17,4 @@ public function rules(): array
'reason' => ['required', 'string', 'max:500'],
];
}
/**
* @return array<string, string>
*/
public function attributes(): array
{
return [
'reason' => 'alasan penolakan',
];
}
}

View File

@ -3,9 +3,7 @@
namespace App\Http\Requests\Admin\Manage;
use App\Enums\Permission;
use App\Enums\ProductStockQuality;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StokOpnameAutoSaveRequest extends FormRequest
{
@ -27,7 +25,7 @@ public function rules(): array
'notes' => ['nullable', 'string', 'max:1000'],
'items' => ['nullable', 'array'],
'items.*.product_variant_id' => ['required', 'integer', 'exists:product_variants,id'],
'items.*.stock_quality' => ['required', Rule::enum(ProductStockQuality::class)],
'items.*.stock_quality' => ['required', \Illuminate\Validation\Rule::enum(\App\Enums\ProductStockQuality::class)],
'items.*.physical_stock' => ['nullable', 'integer', 'min:0'],
'items.*.notes' => ['nullable', 'string', 'max:500'],
];

View File

@ -3,9 +3,7 @@
namespace App\Http\Requests\Admin\Manage;
use App\Enums\Permission;
use App\Enums\ProductStockQuality;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StokOpnameRequest extends FormRequest
{
@ -25,7 +23,7 @@ public function rules(): array
'notes' => ['nullable', 'string', 'max:1000'],
'items' => ['required', 'array', 'min:1'],
'items.*.product_variant_id' => ['required', 'integer', 'exists:product_variants,id'],
'items.*.stock_quality' => ['required', Rule::enum(ProductStockQuality::class)],
'items.*.stock_quality' => ['required', \Illuminate\Validation\Rule::enum(\App\Enums\ProductStockQuality::class)],
'items.*.physical_stock' => ['required', 'integer', 'min:0'],
'items.*.notes' => ['nullable', 'string', 'max:500'],
];

View File

@ -36,7 +36,6 @@ public function getProductNameAttribute(): string
public function getVariantNameAttribute(): string
{
$qualityLabel = $this->stock_quality ? ' (' . $this->stock_quality->label() . ')' : '';
return $this->productVariant->name . $qualityLabel;
}

View File

@ -1,49 +0,0 @@
<?php
namespace App\Services\Concerns;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
trait CachesQuery
{
protected function cacheRemember(string $key, int $ttl, callable $callback)
{
return Cache::remember($key, $ttl, $callback);
}
protected function cacheForever(string $key, callable $callback)
{
return Cache::rememberForever($key, $callback);
}
protected function cacheForget(string $key): void
{
Cache::forget($key);
}
protected function cacheForgetByPattern(string $pattern): void
{
if (config('cache.default') !== 'redis') {
Cache::flush();
return;
}
$redis = Redis::connection('cache');
$prefix = config('cache.prefix', 'laravel_cache');
$fullPattern = "{$prefix}:{$pattern}";
$cursor = null;
do {
[$cursor, $keys] = $redis->scan($cursor ?? 0, ['match' => $fullPattern, 'count' => 100]);
if (! empty($keys)) {
$redis->del($keys);
}
} while ($cursor);
}
}

View File

@ -6,7 +6,6 @@
use App\Models\CashAccount;
use App\Models\CashTransaction;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\Concerns\RunsInTransaction;
use App\Services\Concerns\SyncsPhotos;
use App\Services\Media\MediaService;
@ -19,7 +18,7 @@
class CashService
{
use CachesQuery, RunsInTransaction, SyncsPhotos;
use RunsInTransaction, SyncsPhotos;
private const MAX_PHOTOS = 1;
@ -30,9 +29,7 @@ public function __construct(
public function getDefaultAccount(): CashAccount
{
return $this->cacheRemember('finance:default_account', 86400, function (): CashAccount {
return CashAccount::query()->firstOrFail();
});
}
public function paginateForIndex(CashAccount $cashAccount, array $tableQuery, string $referenceType = ''): LengthAwarePaginator

View File

@ -13,7 +13,6 @@
use App\Models\PayrollAdjustment;
use App\Models\PayrollPeriod;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\Concerns\RunsInTransaction;
use App\Services\System\PushNotificationService;
use Carbon\Carbon;
@ -23,7 +22,7 @@
class PayrollService
{
use CachesQuery, RunsInTransaction;
use RunsInTransaction;
public function __construct(
private readonly CashService $cashService,
@ -32,12 +31,10 @@ public function __construct(
public function listPeriods(): Collection
{
return $this->cacheRemember('payroll:periods', 3600, function (): Collection {
return PayrollPeriod::query()
->orderByDesc('year')
->orderByDesc('month')
->get();
});
}
public function resolvePeriod(?int $periodId): ?PayrollPeriod
@ -46,7 +43,6 @@ public function resolvePeriod(?int $periodId): ?PayrollPeriod
return PayrollPeriod::query()->find($periodId);
}
return $this->cacheRemember('payroll:current_period', 3600, function (): ?PayrollPeriod {
return PayrollPeriod::query()
->where('status', PayrollPeriodStatus::OPEN)
->orderByDesc('year')
@ -56,7 +52,6 @@ public function resolvePeriod(?int $periodId): ?PayrollPeriod
->orderByDesc('year')
->orderByDesc('month')
->first();
});
}
public function periodSummary(PayrollPeriod $period, User $user): array
@ -166,8 +161,6 @@ function () use ($user): PayrollPeriod {
'Gagal membuka periode payroll',
);
$this->cacheForgetByPattern('payroll:*');
return $period;
}
@ -219,8 +212,6 @@ function () use ($payroll, $validated, $user): void {
'Gagal menambahkan penyesuaian gaji',
);
$this->cacheForgetByPattern('payroll:*');
if ($payroll->employee?->user_id) {
$typeLabel = PayrollAdjustmentType::from($validated['type'])->label();
$formattedAmount = 'Rp '.number_format($validated['amount'], 0, ',', '.');
@ -252,8 +243,6 @@ function () use ($payroll, $adjustment, $validated): void {
'Gagal memperbarui penyesuaian gaji',
);
$this->cacheForgetByPattern('payroll:*');
if ($payroll->employee?->user_id) {
$typeLabel = PayrollAdjustmentType::from($validated['type'])->label();
$formattedAmount = 'Rp '.number_format($validated['amount'], 0, ',', '.');
@ -282,8 +271,6 @@ function () use ($payroll, $adjustment): void {
'Gagal menghapus penyesuaian gaji',
);
$this->cacheForgetByPattern('payroll:*');
if ($payroll->employee?->user_id) {
$this->pushNotificationService->sendToUser(
'📊 Penyesuaian Gaji Dihapus',
@ -311,8 +298,6 @@ function () use ($payroll, $user): void {
'Gagal membayar gaji (total 0)',
);
$this->cacheForgetByPattern('payroll:*');
return;
}
@ -342,8 +327,6 @@ function () use ($payroll, $user): void {
'Gagal membayar gaji',
);
$this->cacheForgetByPattern('payroll:*');
if ($payroll->employee?->user_id) {
$this->pushNotificationService->sendToUser(
'💸 Gaji Dibayarkan',

View File

@ -6,7 +6,6 @@
use App\Models\Employee;
use App\Models\User;
use App\Models\UserProfile;
use App\Services\Concerns\CachesQuery;
use App\Services\Concerns\RunsInTransaction;
use App\Services\Concerns\SyncsPhotos;
use App\Services\Media\MediaService;
@ -18,7 +17,7 @@
class EmployeeService
{
use CachesQuery, RunsInTransaction, SyncsPhotos;
use RunsInTransaction, SyncsPhotos;
public function __construct(
private readonly MediaService $mediaService,
@ -61,9 +60,7 @@ public function paginateForIndex(
public function assignableRoleOptions(): array
{
return $this->cacheRemember('system:assignable_roles', 86400, function (): array {
return Role::assignableSelectOptions();
});
}
public function findForEdit(User $user): array
@ -112,8 +109,6 @@ function () use ($validated): void {
},
'Gagal membuat karyawan',
);
$this->cacheForgetByPattern('hr:employees:*');
}
public function update(User $user, array $validated): void
@ -167,8 +162,6 @@ function () use ($validated, $user, $employee): void {
},
'Gagal memperbarui karyawan',
);
$this->cacheForgetByPattern('hr:employees:*');
}
public function toggleStatus(User $user, array $validated): void
@ -185,8 +178,6 @@ function () use ($user, $validated): void {
},
'Gagal memperbarui status karyawan',
);
$this->cacheForgetByPattern('hr:employees:*');
}
public function resetPassword(User $user): void
@ -201,8 +192,6 @@ function () use ($user): void {
},
'Gagal mereset kata sandi karyawan',
);
$this->cacheForgetByPattern('hr:employees:*');
}
public function delete(User $user): void
@ -215,8 +204,6 @@ function () use ($user): void {
},
'Gagal menghapus karyawan',
);
$this->cacheForgetByPattern('hr:employees:*');
}
private function syncProfilePhoto(UserProfile $profile, array $validated): void

View File

@ -5,13 +5,10 @@
use App\Enums\PriceType;
use App\Models\CuttingResultPrice;
use App\Models\ProductPrice;
use App\Services\Concerns\CachesQuery;
use Illuminate\Support\Collection;
class CuttingResultPriceResolver
{
use CachesQuery;
public function resolve(int $productVariantId, PriceType $priceType): ?CuttingResultPrice
{
$price = CuttingResultPrice::query()
@ -46,7 +43,6 @@ public function resolve(int $productVariantId, PriceType $priceType): ?CuttingRe
public function latestPricesForVariant(int $productVariantId): array
{
return $this->cacheRemember("prices:variant:{$productVariantId}", 900, function () use ($productVariantId) {
$prices = [];
foreach (PriceType::cases() as $priceType) {
@ -58,12 +54,10 @@ public function latestPricesForVariant(int $productVariantId): array
}
return $prices;
});
}
public function latestPricesForVariants(array $variantIds): Collection
{
return $this->cacheRemember('prices:variants:'.md5(implode(',', $variantIds)), 900, function () use ($variantIds) {
if (empty($variantIds)) {
return collect();
}
@ -101,6 +95,5 @@ public function latestPricesForVariants(array $variantIds): Collection
}
return $results->groupBy('product_variant_id');
});
}
}

View File

@ -15,7 +15,6 @@
use App\Models\RawMaterial;
use App\Models\RawMaterialPrice;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\Concerns\RunsInTransaction;
use App\Services\Media\MediaService;
use App\Services\System\PushNotificationService;
@ -27,7 +26,7 @@
class CuttingService
{
use CachesQuery, RunsInTransaction;
use RunsInTransaction;
public function __construct(
private readonly PushNotificationService $pushNotificationService,
@ -556,8 +555,6 @@ function () use ($validated, $user): Cutting {
route('admin.manage.cuttings.index'),
);
$this->cacheForgetByPattern('manage:cuttings:*');
return $cutting;
}
@ -653,8 +650,6 @@ function () use ($cutting, $validated): void {
['owner', 'developer', 'direktur'],
route('admin.manage.cuttings.index'),
);
$this->cacheForgetByPattern('manage:cuttings:*');
}
public function delete(Cutting $cutting): void
@ -691,8 +686,6 @@ function () use ($cutting): void {
['owner', 'developer', 'direktur'],
route('admin.manage.cuttings.index'),
);
$this->cacheForgetByPattern('manage:cuttings:*');
}
public function transitionStatus(
@ -792,9 +785,6 @@ function () use ($cutting, $status, $verificationNote, $results, $resultPrices,
route('admin.manage.stocks.index'),
);
}
$this->cacheForgetByPattern('manage:cuttings:*');
$this->cacheForgetByPattern('prices:*');
}
private function buildMaterials(array $materials): array
@ -1227,8 +1217,6 @@ public function quickCreateRawMaterial(array $validated): array
];
}
$this->cacheForgetByPattern('manage:cuttings:*');
return [
'id' => $rawMaterial->id,
'name' => $rawMaterial->name,
@ -1295,8 +1283,6 @@ public function quickCreateProduct(array $validated): array
];
}
$this->cacheForgetByPattern('manage:cuttings:*');
return [
'id' => $product->id,
'name' => $product->name,

View File

@ -13,7 +13,6 @@
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\Concerns\RunsInTransaction;
use App\Services\Finance\CashService;
use App\Services\Media\MediaService;
@ -28,7 +27,7 @@
class OrderService
{
use CachesQuery, RunsInTransaction;
use RunsInTransaction;
private const MAX_PHOTOS = 1;
@ -497,8 +496,6 @@ function () use ($validated, $user): Order {
route('admin.manage.orders.index'),
);
$this->cacheForgetByPattern('manage:orders:*');
return $order;
}
@ -587,8 +584,6 @@ function () use ($order, $validated): void {
['owner', 'developer', 'direktur'],
route('admin.manage.orders.index'),
);
$this->cacheForgetByPattern('manage:orders:*');
}
public function delete(Order $order): void
@ -622,8 +617,6 @@ function () use ($order): void {
['owner', 'developer', 'direktur'],
route('admin.manage.orders.index'),
);
$this->cacheForgetByPattern('manage:orders:*');
}
public function transitionStatus(Order $order, OrderStatus $status): void
@ -661,8 +654,6 @@ function () use ($order, $status): void {
['owner', 'developer', 'direktur'],
route('admin.manage.orders.index'),
);
$this->cacheForgetByPattern('manage:orders:*');
}
private function buildLineItems(array $items, PriceType $priceType): array

View File

@ -12,7 +12,6 @@
use App\Models\Purchase;
use App\Models\RawMaterial;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\Master\ProductService;
use App\Services\Master\RawMaterialService;
use App\Services\System\PushNotificationService;
@ -30,8 +29,6 @@
class OwnerVerificationService
{
use CachesQuery;
public function __construct(
private readonly StockService $stockService,
private readonly ProductService $productService,
@ -132,7 +129,6 @@ private function pendingCuttingRows(User $user, array $tableQuery): Collection
public function pendingCountForUser(User $user): int
{
return $this->cacheRemember("verification:pending_count:{$user->id}", 30, function () use ($user) {
$requestCount = OwnerVerificationRequest::query()
->pending()
->visibleTo($user)
@ -143,7 +139,6 @@ public function pendingCountForUser(User $user): int
}
return $requestCount + Cutting::query()->pendingVerification()->count();
});
}
public function subjectTypeOptions(User $user): array
@ -222,8 +217,6 @@ public function approveRequest(
'✅ Pengajuan Disetujui',
"Pengajuan {$request->action->label()} {$subjectLabel} '{$this->requestTitle($request)}' telah disetujui owner.",
);
$this->cacheForgetByPattern('verification:*');
}
public function rejectRequest(
@ -281,8 +274,6 @@ public function rejectRequest(
'❌ Pengajuan Ditolak',
"Pengajuan {$request->action->label()} {$subjectLabel} '{$title}' ditolak dengan alasan: '{$reason}'.",
);
$this->cacheForgetByPattern('verification:*');
}
private function rejectVerificationRequest(OwnerVerificationRequest $request): void

View File

@ -12,7 +12,6 @@
use App\Models\RawMaterialPrice;
use App\Models\Supplier;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\Concerns\RunsInTransaction;
use App\Services\Media\MediaService;
use App\Services\System\PushNotificationService;
@ -24,7 +23,7 @@
class PurchaseService
{
use CachesQuery, RunsInTransaction;
use RunsInTransaction;
private const MAX_PHOTOS = 1;
@ -325,8 +324,6 @@ function () use ($validated, $user, $isOwner): Purchase {
);
}
$this->cacheForgetByPattern('manage:purchases:*');
return $purchase;
}
@ -376,8 +373,6 @@ function () use ($purchase, $validated, $user, $isOwner): void {
$purchase->supplier->name,
);
}
$this->cacheForgetByPattern('manage:purchases:*');
}
public function delete(Purchase $purchase, User $user): void
@ -386,7 +381,6 @@ public function delete(Purchase $purchase, User $user): void
if ($isOwner) {
$this->executeDelete($purchase);
$this->cacheForgetByPattern('manage:purchases:*');
return;
}
@ -431,8 +425,6 @@ public function applyVerificationRequest(OwnerVerificationRequest $verificationR
'action' => 'Aksi verifikasi belanja tidak didukung.',
]),
};
$this->cacheForgetByPattern('manage:purchases:*');
}
public function rejectVerificationRequest(OwnerVerificationRequest $verificationRequest): void
@ -444,8 +436,6 @@ public function rejectVerificationRequest(OwnerVerificationRequest $verification
'action' => 'Aksi verifikasi belanja tidak didukung.',
]),
};
$this->cacheForgetByPattern('manage:purchases:*');
}
public function clearVerificationRequestMedia(OwnerVerificationRequest $verificationRequest): void

View File

@ -10,7 +10,6 @@
use App\Models\ProductPrice;
use App\Models\ProductVariant;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\System\PushNotificationService;
use App\Support\Media\MediaPresenter;
use Illuminate\Support\Collection;
@ -20,8 +19,6 @@
class StockService
{
use CachesQuery;
public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
@ -134,8 +131,6 @@ public function submitVerification(
['owner', 'developer', 'direktur'],
route('admin.manage.stocks.index'),
);
$this->cacheForgetByPattern('manage:stocks:*');
}
public function approveVerification(
@ -186,9 +181,6 @@ public function approveVerification(
['owner', 'developer', 'direktur'],
route('admin.manage.stocks.index'),
);
$this->cacheForgetByPattern('manage:stocks:*');
$this->cacheForgetByPattern('prices:*');
}
public function rejectVerification(
@ -240,8 +232,6 @@ public function rejectVerification(
route('admin.manage.stocks.index'),
);
}
$this->cacheForgetByPattern('manage:stocks:*');
}
private function breakMaterialCircularReference(CuttingMaterial $material): void

View File

@ -3,13 +3,11 @@
namespace App\Services\Manage;
use App\Enums\Permission;
use App\Enums\ProductStockQuality;
use App\Enums\StokOpnameStatus;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\StokOpname;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\System\PushNotificationService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
@ -19,8 +17,6 @@
class StokOpnameService
{
use CachesQuery;
public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
@ -110,7 +106,7 @@ public function findForEdit(StokOpname $stokOpname): array
public function create(array $validated, User $user): StokOpname
{
try {
$stokOpname = DB::transaction(function () use ($validated, $user): StokOpname {
return DB::transaction(function () use ($validated, $user): StokOpname {
$stokOpname = StokOpname::create([
'opname_date' => $validated['opname_date'],
'notes' => $validated['notes'],
@ -122,10 +118,6 @@ public function create(array $validated, User $user): StokOpname
return $stokOpname;
});
$this->cacheForgetByPattern('manage:stok_opname:*');
return $stokOpname;
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
@ -168,8 +160,6 @@ public function update(StokOpname $stokOpname, array $validated): void
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
$this->cacheForgetByPattern('manage:stok_opname:*');
}
public function delete(StokOpname $stokOpname): void
@ -196,8 +186,6 @@ public function delete(StokOpname $stokOpname): void
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
$this->cacheForgetByPattern('manage:stok_opname:*');
}
public function submit(StokOpname $stokOpname, User $user): void
@ -238,8 +226,6 @@ public function submit(StokOpname $stokOpname, User $user): void
['owner', 'developer', 'admin-toko'],
route('admin.manage.stok-opnames.index'),
);
$this->cacheForgetByPattern('manage:stok_opname:*');
}
public function verify(StokOpname $stokOpname, User $user, ?string $verificationNotes = null): void
@ -255,9 +241,9 @@ public function verify(StokOpname $stokOpname, User $user, ?string $verification
foreach ($stokOpname->items as $item) {
if ($item->difference !== 0) {
$column = match ($item->stock_quality) {
ProductStockQuality::GOOD => 'stock',
ProductStockQuality::RETAIL => 'retail_stock',
ProductStockQuality::REJECT => 'reject_stock',
\App\Enums\ProductStockQuality::GOOD => 'stock',
\App\Enums\ProductStockQuality::RETAIL => 'retail_stock',
\App\Enums\ProductStockQuality::REJECT => 'reject_stock',
};
$item->productVariant()->update([
$column => $item->physical_stock,
@ -289,8 +275,6 @@ public function verify(StokOpname $stokOpname, User $user, ?string $verification
$stokOpname->created_by_id,
route('admin.manage.stok-opnames.index'),
);
$this->cacheForgetByPattern('manage:stok_opname:*');
}
public function reject(StokOpname $stokOpname, User $user, string $reason): void
@ -327,14 +311,12 @@ public function reject(StokOpname $stokOpname, User $user, string $reason): void
$stokOpname->created_by_id,
route('admin.manage.stok-opnames.index'),
);
$this->cacheForgetByPattern('manage:stok_opname:*');
}
public function autoSave(array $validated, User $user): StokOpname
{
try {
$stokOpname = DB::transaction(function () use ($validated, $user): StokOpname {
return DB::transaction(function () use ($validated, $user): StokOpname {
$stokOpname = null;
if (! empty($validated['stok_opname_id'])) {
@ -360,10 +342,6 @@ public function autoSave(array $validated, User $user): StokOpname
return $stokOpname;
});
$this->cacheForgetByPattern('manage:stok_opname:*');
return $stokOpname;
} catch (\Throwable $e) {
Log::error('Gagal auto-save stok opname: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
@ -383,11 +361,11 @@ private function syncItems(StokOpname $stokOpname, array $items): void
continue;
}
$quality = ProductStockQuality::from($item['stock_quality']);
$quality = \App\Enums\ProductStockQuality::from($item['stock_quality']);
$column = match ($quality) {
ProductStockQuality::GOOD => 'stock',
ProductStockQuality::RETAIL => 'retail_stock',
ProductStockQuality::REJECT => 'reject_stock',
\App\Enums\ProductStockQuality::GOOD => 'stock',
\App\Enums\ProductStockQuality::RETAIL => 'retail_stock',
\App\Enums\ProductStockQuality::REJECT => 'reject_stock',
};
$systemStock = $variant->$column ?? 0;

View File

@ -3,14 +3,11 @@
namespace App\Services\Master;
use App\Models\Category;
use App\Services\Concerns\CachesQuery;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
class CategoryService
{
use CachesQuery;
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
{
$query = Category::query()
@ -31,27 +28,20 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
public function create(array $validated): void
{
Category::create($validated);
$this->cacheForget('master:categories:options');
$this->cacheForget('homepage:page_data');
}
public function update(Category $category, array $validated): void
{
$category->update($validated);
$this->cacheForget('master:categories:options');
$this->cacheForget('homepage:page_data');
}
public function delete(Category $category): void
{
$category->delete();
$this->cacheForget('master:categories:options');
$this->cacheForget('homepage:page_data');
}
public function getSelectOptions(): array
{
return $this->cacheRemember('master:categories:options', 3600, function () {
return Category::query()
->orderBy('name')
->get(['id', 'name'])
@ -60,7 +50,6 @@ public function getSelectOptions(): array
'label' => $category->name,
])
->all();
});
}
private function applySorting(Builder $query, string $sort, string $direction): void

View File

@ -3,14 +3,11 @@
namespace App\Services\Master;
use App\Models\Customer;
use App\Services\Concerns\CachesQuery;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
class CustomerService
{
use CachesQuery;
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
{
$query = Customer::query()
@ -33,32 +30,25 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
public function create(array $validated): void
{
Customer::create($validated);
$this->cacheForget('master:customers:options');
}
public function createAndReturn(array $validated): Customer
{
$customer = Customer::create($validated);
$this->cacheForget('master:customers:options');
return $customer;
return Customer::create($validated);
}
public function update(Customer $customer, array $validated): void
{
$customer->update($validated);
$this->cacheForget('master:customers:options');
}
public function delete(Customer $customer): void
{
$customer->delete();
$this->cacheForget('master:customers:options');
}
public function getSelectOptions(): array
{
return $this->cacheRemember('master:customers:options', 3600, function () {
return Customer::query()
->orderBy('name')
->get(['id', 'name'])
@ -67,7 +57,6 @@ public function getSelectOptions(): array
'label' => $customer->name,
])
->all();
});
}
private function applySorting(Builder $query, string $sort, string $direction): void

View File

@ -10,7 +10,6 @@
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\Concerns\RunsInTransaction;
use App\Services\Media\MediaService;
use App\Services\System\PushNotificationService;
@ -21,7 +20,7 @@
class ProductService
{
use CachesQuery, RunsInTransaction;
use RunsInTransaction;
private const MAX_VARIANT_IMAGES = 5;
@ -167,10 +166,6 @@ function () use ($validated, $user, $isOwner): void {
'Gagal membuat produk',
);
if ($isOwner) {
$this->cacheForgetByPattern('master:products:*');
}
if (! $isOwner) {
$this->notifyForPendingRequest(
$user,
@ -245,9 +240,6 @@ function () use ($validated, $product, $user, $isOwner): void {
'Gagal memperbarui produk',
);
$this->cacheForgetByPattern('master:products:*');
$this->cacheForget('homepage:page_data');
if (! $isOwner) {
$this->notifyForPendingRequest(
$user,
@ -265,8 +257,6 @@ public function delete(Product $product, User $user): void
if ($isOwner) {
$this->applyDeleteSubject($product);
$this->cacheForgetByPattern('master:products:*');
$this->cacheForget('homepage:page_data');
return;
}
@ -306,9 +296,6 @@ public function toggleStatus(Product $product, array $validated, User $user): vo
'is_active' => (bool) $validated['is_active'],
]);
$this->cacheForgetByPattern('master:products:*');
$this->cacheForget('homepage:page_data');
return;
}
@ -357,8 +344,6 @@ public function applyVerificationRequest(OwnerVerificationRequest $verificationR
'action' => 'Aksi verifikasi produk tidak didukung.',
]),
};
$this->cacheForgetByPattern('master:products:*');
}
public function rejectVerificationRequest(OwnerVerificationRequest $verificationRequest): void
@ -371,9 +356,6 @@ public function rejectVerificationRequest(OwnerVerificationRequest $verification
'action' => 'Aksi verifikasi produk tidak didukung.',
]),
};
$this->cacheForgetByPattern('master:products:*');
$this->cacheForget('homepage:page_data');
}
public function clearVerificationRequestMedia(OwnerVerificationRequest $verificationRequest): void

View File

@ -10,7 +10,6 @@
use App\Models\RawMaterial;
use App\Models\RawMaterialPrice;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\Concerns\RunsInTransaction;
use App\Services\Media\MediaService;
use App\Services\System\PushNotificationService;
@ -21,7 +20,7 @@
class RawMaterialService
{
use CachesQuery, RunsInTransaction;
use RunsInTransaction;
private const MAX_VARIANT_IMAGES = 5;
@ -145,10 +144,6 @@ function () use ($validated, $user, $isOwner): void {
'Gagal membuat bahan baku',
);
if ($isOwner) {
$this->cacheForgetByPattern('master:raw_materials:*');
}
if (! $isOwner) {
$this->notifyForPendingRequest(
$user,
@ -202,10 +197,6 @@ function () use ($validated, $rawMaterial, $user, $isOwner): void {
'Gagal memperbarui bahan baku',
);
if ($isOwner) {
$this->cacheForgetByPattern('master:raw_materials:*');
}
if (! $isOwner) {
$this->notifyForPendingRequest(
$user,
@ -223,7 +214,6 @@ public function delete(RawMaterial $rawMaterial, User $user): void
if ($isOwner) {
$this->applyDeleteSubject($rawMaterial);
$this->cacheForgetByPattern('master:raw_materials:*');
return;
}
@ -263,8 +253,6 @@ public function toggleStatus(RawMaterial $rawMaterial, array $validated, User $u
'is_active' => (bool) $validated['is_active'],
]);
$this->cacheForgetByPattern('master:raw_materials:*');
return;
}
@ -313,8 +301,6 @@ public function applyVerificationRequest(OwnerVerificationRequest $verificationR
'action' => 'Aksi verifikasi bahan baku tidak didukung.',
]),
};
$this->cacheForgetByPattern('master:raw_materials:*');
}
public function rejectVerificationRequest(OwnerVerificationRequest $verificationRequest): void
@ -327,8 +313,6 @@ public function rejectVerificationRequest(OwnerVerificationRequest $verification
'action' => 'Aksi verifikasi bahan baku tidak didukung.',
]),
};
$this->cacheForgetByPattern('master:raw_materials:*');
}
public function clearVerificationRequestMedia(OwnerVerificationRequest $verificationRequest): void

View File

@ -3,14 +3,11 @@
namespace App\Services\Master;
use App\Models\Supplier;
use App\Services\Concerns\CachesQuery;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
class SupplierService
{
use CachesQuery;
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
{
$query = Supplier::query()
@ -33,24 +30,20 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
public function create(array $validated): void
{
Supplier::create($validated);
$this->cacheForget('master:suppliers:options');
}
public function update(Supplier $supplier, array $validated): void
{
$supplier->update($validated);
$this->cacheForget('master:suppliers:options');
}
public function delete(Supplier $supplier): void
{
$supplier->delete();
$this->cacheForget('master:suppliers:options');
}
public function getSelectOptions(): array
{
return $this->cacheRemember('master:suppliers:options', 3600, function () {
return Supplier::query()
->orderBy('name')
->get(['id', 'name'])
@ -59,7 +52,6 @@ public function getSelectOptions(): array
'label' => $supplier->name,
])
->all();
});
}
private function applySorting(Builder $query, string $sort, string $direction): void

View File

@ -20,17 +20,13 @@
use App\Models\Purchase;
use App\Models\RawMaterialPrice;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class AnalysisService
{
use CachesQuery;
public function getAttendance(): array
{
return $this->cacheRemember('analysis:get_attendance', 900, function () {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
@ -61,12 +57,10 @@ public function getAttendance(): array
'absent' => $absent,
'on_leave' => $onLeave,
];
});
}
public function getMyAttendance(User $user, ?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_my_attendance', 900, function () use ($user, $startDate, $endDate) {
$employee = $user->employee;
if ($employee === null) {
@ -126,12 +120,10 @@ public function getMyAttendance(User $user, ?Carbon $startDate = null, ?Carbon $
'leave_days' => $leaveDays,
'percentage' => $percentage,
];
});
}
public function getCashOverview(): array
{
return $this->cacheRemember('analysis:get_cash_overview', 900, function () {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
@ -159,12 +151,10 @@ public function getCashOverview(): array
'total_deposit' => (int) ($summary->total_deposit ?? 0),
'total_withdrawal' => (int) ($summary->total_withdrawal ?? 0),
];
});
}
public function getTopSuppliers(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_top_suppliers', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
@ -184,12 +174,10 @@ public function getTopSuppliers(?Carbon $startDate = null, ?Carbon $endDate = nu
'purchase_count' => (int) $item->purchase_count,
])
->toArray();
});
}
public function getTopCustomers(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_top_customers', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
@ -212,12 +200,10 @@ public function getTopCustomers(?Carbon $startDate = null, ?Carbon $endDate = nu
'order_count' => (int) $item->order_count,
])
->toArray();
});
}
public function getRevenueSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_revenue_summary', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
@ -262,12 +248,10 @@ public function getRevenueSummary(?Carbon $startDate = null, ?Carbon $endDate =
'total_orders' => (int) ($revenueSummary->total_orders ?? 0),
'avg_order' => (int) ($revenueSummary->avg_order ?? 0),
];
});
}
public function getMonthlyRevenue(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_monthly_revenue', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
@ -381,12 +365,10 @@ public function getMonthlyRevenue(?Carbon $startDate = null, ?Carbon $endDate =
}
return $result;
});
}
public function getExpenseSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_expense_summary', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
@ -423,12 +405,10 @@ public function getExpenseSummary(?Carbon $startDate = null, ?Carbon $endDate =
'expense_total' => $expenseTotal,
'advance_total' => $advanceTotal,
];
});
}
public function getMonthlyExpense(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_monthly_expense', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
@ -498,12 +478,10 @@ public function getMonthlyExpense(?Carbon $startDate = null, ?Carbon $endDate =
}
return $result;
});
}
public function getProfitMetrics(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_profit_metrics', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
@ -605,12 +583,10 @@ public function getProfitMetrics(?Carbon $startDate = null, ?Carbon $endDate = n
'aov' => $aov,
'items_per_transaction' => $itemsPerTransaction,
];
});
}
public function getRawMaterialStock(): array
{
return $this->cacheRemember('analysis:get_raw_material_stock', 900, function () {
$prices = RawMaterialPrice::query()
->join('raw_materials', 'raw_material_prices.raw_material_id', '=', 'raw_materials.id')
->selectRaw('
@ -634,12 +610,10 @@ public function getRawMaterialStock(): array
'kilogram' => round((float) ($prices->get('kilogram')->total_stock ?? 0), 2),
],
];
});
}
public function getProductStock(): array
{
return $this->cacheRemember('analysis:get_product_stock', 900, function () {
$variants = ProductVariant::query()
->join('products', 'product_variants.product_id', '=', 'products.id')
->selectRaw('
@ -672,12 +646,10 @@ public function getProductStock(): array
'total_variants' => (int) ($variants->total_variants ?? 0),
'total_categories' => (int) $totalCategories,
];
});
}
public function getBusyHours(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_busy_hours', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
@ -702,12 +674,10 @@ public function getBusyHours(?Carbon $startDate = null, ?Carbon $endDate = null)
}
return $result;
});
}
public function getTopProducts(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_top_products', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
@ -732,12 +702,10 @@ public function getTopProducts(?Carbon $startDate = null, ?Carbon $endDate = nul
'total_revenue' => (int) $item->total_revenue,
])
->toArray();
});
}
public function getMarketingSales(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_marketing_sales', 900, function () use ($startDate, $endDate) {
$qtySubquery = DB::table('order_items')
->select('order_id', DB::raw('SUM(quantity) as total_qty'))
->whereNull('deleted_at')
@ -775,7 +743,6 @@ public function getMarketingSales(?Carbon $startDate = null, ?Carbon $endDate =
'avg_order' => (int) $item->avg_order,
])
->toArray();
});
}
public function isManager(?User $user): bool

View File

@ -5,7 +5,6 @@
use App\Models\Category;
use App\Models\Product;
use App\Models\SystemConfiguration;
use App\Services\Concerns\CachesQuery;
use App\Services\Manage\CuttingResultPriceResolver;
use App\Services\System\Setting\HomepageSettingService;
use App\Settings\SocialMediaSettings;
@ -14,8 +13,6 @@
class HomepageService
{
use CachesQuery;
public function __construct(
private readonly CuttingResultPriceResolver $cuttingResultPriceResolver,
private readonly HomepageSettingService $homepageSettingService,
@ -23,7 +20,6 @@ public function __construct(
public function pageData(): array
{
return $this->cacheRemember('homepage:page_data', 900, function () {
$categories = Category::getActiveWithProducts();
$products = $this->getProducts();
@ -48,7 +44,6 @@ public function pageData(): array
'tiktokUrl' => $socialSettings->tiktok_url ?? null,
'homepage' => $this->homepageSettingService->homepageData(),
];
});
}
private function getProducts()

View File

@ -4,7 +4,6 @@
use App\Enums\Permission as PermissionEnum;
use App\Enums\Role as EnumsRole;
use App\Services\Concerns\CachesQuery;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
@ -15,8 +14,6 @@
class RoleService
{
use CachesQuery;
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
{
$query = Role::query()
@ -37,7 +34,6 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
public function permissionOptions(): array
{
return $this->cacheRemember('system:permissions', 86400, function (): array {
return collect(PermissionEnum::cases())
->map(fn (PermissionEnum $permission) => [
'value' => $permission->value,
@ -46,7 +42,6 @@ public function permissionOptions(): array
])
->values()
->all();
});
}
public function findForEdit(Role $role): array
@ -86,8 +81,6 @@ public function create(array $validated): void
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
$this->cacheForgetByPattern('system:roles:*');
}
public function update(Role $role, array $validated): void
@ -113,15 +106,11 @@ public function update(Role $role, array $validated): void
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
$this->cacheForgetByPattern('system:roles:*');
}
public function delete(Role $role): void
{
$role->delete();
$this->cacheForgetByPattern('system:roles:*');
}
private function applySorting(Builder $query, string $sort, string $direction): void

View File

@ -3,7 +3,6 @@
namespace App\Services\System\Setting;
use App\Models\HomepageConfiguration;
use App\Services\Concerns\CachesQuery;
use App\Services\Concerns\RunsInTransaction;
use App\Services\Concerns\SyncsPhotos;
use App\Services\Media\MediaService;
@ -11,7 +10,7 @@
class HomepageSettingService
{
use CachesQuery, RunsInTransaction, SyncsPhotos;
use RunsInTransaction, SyncsPhotos;
public function __construct(
private readonly MediaService $mediaService,
@ -19,7 +18,6 @@ public function __construct(
public function homepageData(): array
{
return $this->cacheRemember('homepage:settings', 86400, function () {
$configuration = HomepageConfiguration::instance();
$configuration->load('media');
@ -32,7 +30,6 @@ public function homepageData(): array
'about_image_url' => $aboutImage['url'] ?? null,
'gallery_images' => $galleryImages,
];
});
}
public function updateHomepage(array $validated): void
@ -77,8 +74,5 @@ function () use ($validated): void {
},
'Gagal memperbarui pengaturan homepage',
);
$this->cacheForget('homepage:settings');
$this->cacheForget('homepage:page_data');
}
}

View File

@ -2,16 +2,12 @@
namespace App\Services\System\Setting;
use App\Services\Concerns\CachesQuery;
use App\Settings\HrSettings;
class HrSettingService
{
use CachesQuery;
public function hrData(): array
{
return $this->cacheRemember('system:hr_settings', 86400, function () {
$settings = app(HrSettings::class);
return [
@ -20,7 +16,6 @@ public function hrData(): array
'late_penalty_amount' => $settings->late_penalty_amount,
'absent_penalty_amount' => $settings->absent_penalty_amount,
];
});
}
public function updateHr(array $validated): void
@ -32,7 +27,5 @@ public function updateHr(array $validated): void
$settings->late_penalty_amount = (int) $validated['late_penalty_amount'];
$settings->absent_penalty_amount = (int) $validated['absent_penalty_amount'];
$settings->save();
$this->cacheForget('system:hr_settings');
}
}

View File

@ -8,7 +8,6 @@
use App\Enums\Permission;
use App\Models\OwnerVerificationRequest;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\Concerns\RunsInTransaction;
use App\Services\System\PushNotificationService;
use App\Settings\MarketplaceSettings;
@ -18,7 +17,7 @@
class MarketplaceService
{
use CachesQuery, RunsInTransaction;
use RunsInTransaction;
public function __construct(
private readonly PushNotificationService $pushNotificationService,
@ -53,7 +52,6 @@ private function shopeeFeeKeys(): array
public function marketplaceData(): array
{
return $this->cacheRemember('system:marketplace', 86400, function () {
$settings = app(MarketplaceSettings::class);
return [
@ -73,7 +71,6 @@ public function marketplaceData(): array
'shopee_pre_order' => $this->presentFeeRule($settings->shopee_pre_order),
'shopee_live_extra' => $this->presentFeeRule($settings->shopee_live_extra),
];
});
}
public function updateMarketplace(array $validated, User $user): void
@ -154,8 +151,6 @@ public function saveSettings(array $validated): void
}
$settings->save();
$this->cacheForget('system:marketplace');
}
public function applyVerificationRequest(OwnerVerificationRequest $request): void
@ -164,7 +159,6 @@ public function applyVerificationRequest(OwnerVerificationRequest $request): voi
function () use ($request): void {
$newPayload = $request->payload['new'] ?? [];
$this->saveSettings($newPayload);
$this->cacheForget('system:marketplace');
},
'Gagal menerapkan pengajuan verifikasi owner',
);

View File

@ -2,16 +2,12 @@
namespace App\Services\System\Setting;
use App\Services\Concerns\CachesQuery;
use App\Settings\SocialMediaSettings;
class SocialMediaService
{
use CachesQuery;
public function socialMediaData(): array
{
return $this->cacheRemember('system:social_media', 86400, function () {
$settings = app(SocialMediaSettings::class);
return [
@ -19,7 +15,6 @@ public function socialMediaData(): array
'facebook_url' => $settings->facebook_url,
'tiktok_url' => $settings->tiktok_url,
];
});
}
public function updateSocialMedia(array $validated): void
@ -31,8 +26,5 @@ public function updateSocialMedia(array $validated): void
$settings->tiktok_url = $validated['tiktok_url'] ?? null;
$settings->save();
$this->cacheForget('system:social_media');
$this->cacheForget('homepage:page_data');
}
}

View File

@ -3,7 +3,6 @@
namespace App\Services\System\Setting;
use App\Models\SystemConfiguration;
use App\Services\Concerns\CachesQuery;
use App\Services\Concerns\RunsInTransaction;
use App\Services\Concerns\SyncsPhotos;
use App\Services\Media\MediaService;
@ -13,7 +12,7 @@
class SystemService
{
use CachesQuery, RunsInTransaction, SyncsPhotos;
use RunsInTransaction, SyncsPhotos;
public function __construct(
private readonly MediaService $mediaService,
@ -21,7 +20,6 @@ public function __construct(
public function systemData(): array
{
return $this->cacheRemember('system:settings', 86400, function () {
$settings = app(SystemSettings::class);
$configuration = SystemConfiguration::instance();
$configuration->load('media');
@ -40,7 +38,6 @@ public function systemData(): array
'favicon_url' => $favicon['url'] ?? null,
'login_cover_url' => $loginCover['url'] ?? null,
];
});
}
public function updateSystem(array $validated): void
@ -110,8 +107,5 @@ function () use ($validated): void {
},
'Gagal memperbarui pengaturan sistem',
);
$this->cacheForget('system:settings');
$this->cacheForget('homepage:page_data');
}
}

View File

@ -17,7 +17,6 @@
"league/flysystem-aws-s3-v3": "^3.0",
"league/flysystem-ftp": "^3.0",
"minishlink/web-push": "^9.0",
"predis/predis": "^3.5",
"spatie/laravel-activitylog": "^5.0",
"spatie/laravel-medialibrary": "^11.23",
"spatie/laravel-permission": "^8.0",

65
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "af3de95989c6c9eabd05600486ed22da",
"content-hash": "ea578c35bcf9ab3ac130534272e48937",
"packages": [
{
"name": "aws/aws-crt-php",
@ -3502,69 +3502,6 @@
},
"time": "2026-01-25T14:56:51+00:00"
},
{
"name": "predis/predis",
"version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/predis/predis.git",
"reference": "5c996db191ee2d9bafe651f454b1fca16754271b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/predis/predis/zipball/5c996db191ee2d9bafe651f454b1fca16754271b",
"reference": "5c996db191ee2d9bafe651f454b1fca16754271b",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"psr/http-message": "^1.0|^2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.3",
"phpstan/phpstan": "^1.9",
"phpunit/phpcov": "^6.0 || ^8.0",
"phpunit/phpunit": "^8.0 || ~9.4.4"
},
"suggest": {
"ext-relay": "Faster connection with in-memory caching (>=0.6.2)"
},
"type": "library",
"autoload": {
"psr-4": {
"Predis\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Till Krüss",
"homepage": "https://till.im",
"role": "Maintainer"
}
],
"description": "A flexible and feature-complete Redis/Valkey client for PHP.",
"homepage": "http://github.com/predis/predis",
"keywords": [
"nosql",
"predis",
"redis"
],
"support": {
"issues": "https://github.com/predis/predis/issues",
"source": "https://github.com/predis/predis/tree/v3.5.1"
},
"funding": [
{
"url": "https://github.com/sponsors/tillkruss",
"type": "github"
}
],
"time": "2026-06-11T16:56:53+00:00"
},
{
"name": "psr/clock",
"version": "1.0.0",

View File

@ -100,7 +100,7 @@ function submitReject() {
<div class="space-y-4">
<Field>
<FieldLabel for="owner-verification-reject-reason" required>Alasan Penolakan</FieldLabel>
<FieldLabel for="owner-verification-reject-reason">Alasan Penolakan</FieldLabel>
<Textarea id="owner-verification-reject-reason" v-model="rejectForm.reason"
placeholder="Jelaskan alasan penolakan..." rows="3" />
<FieldError :errors="rejectForm.errors.reason ? [rejectForm.errors.reason] : []" />

View File

@ -338,6 +338,7 @@
->middleware('permission:'.Permission::CUTTINGS_COMPLETE->value.'|'.Permission::CUTTINGS_VERIFY->value.'|'.Permission::CUTTINGS_REJECT->value)
->name('transition_status');
Route::post('draft-materials', [CuttingDraftItemController::class, 'storeMaterial'])
->middleware('permission:'.Permission::CUTTINGS_CREATE->value)
->name('draft_materials.store');

View File

@ -2,7 +2,6 @@
use App\Enums\CuttingStatus;
use App\Enums\Permission as PermissionEnum;
use App\Models\Category;
use App\Models\Cutting;
use App\Models\CuttingMaterial;
use App\Models\CuttingMaterialCombination;
@ -762,7 +761,7 @@ function setupDraftItems(User $user): array
test('quick create product with categories', function () {
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
$category = Category::factory()->create();
$category = \App\Models\Category::factory()->create();
$this->actingAs($user)
->postJson('/admin/manage/cuttings/quick-create-product', [
@ -778,7 +777,7 @@ function setupDraftItems(User $user): array
])
->assertOk();
$product = Product::where('name', 'Product With Category')->first();
$product = \App\Models\Product::where('name', 'Product With Category')->first();
expect($product)->not->toBeNull();
expect($product->categories)->toHaveCount(1);
});

View File

@ -102,7 +102,7 @@ function createCompletedCuttingSetup(): array
'product_variant_id' => $setup['variant']->id,
'good' => 6, // changed from 5 to 6
'reject' => 1, // changed from 2 to 1
],
]
],
'result_prices' => [
[
@ -115,9 +115,9 @@ function createCompletedCuttingSetup(): array
[
'type' => 'retail',
'price' => 75000,
],
],
],
]
]
]
],
];

View File

@ -105,7 +105,7 @@ function createCatalogSetup(): array
'stock_quality' => ProductStockQuality::REJECT->value,
'physical_stock' => 3,
'notes' => 'Kelebihan 1 reject',
],
]
],
];
@ -150,7 +150,7 @@ function createCatalogSetup(): array
'stock_quality' => ProductStockQuality::RETAIL->value,
'physical_stock' => 6,
'notes' => 'Tambah eceran',
],
]
],
];
@ -198,7 +198,7 @@ function createCatalogSetup(): array
'stock_quality' => ProductStockQuality::GOOD->value,
'physical_stock' => 9,
'notes' => 'Diubah jadi 9',
],
]
],
];

View File

@ -1,294 +0,0 @@
<?php
use App\Models\Category;
use App\Models\Customer;
use App\Models\Supplier;
use App\Services\Concerns\CachesQuery;
use App\Services\Master\CategoryService;
use App\Services\Master\CustomerService;
use App\Services\Master\SupplierService;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->seed(RolePermissionSeeder::class);
});
// ─── CachesQuery Trait ─────────────────────────────────────
describe('CachesQuery Trait', function () {
test('cacheRemember stores and retrieves value', function () {
$service = new class
{
use CachesQuery;
public function callRemember(): array
{
return $this->cacheRemember('test:key', 60, function () {
return ['foo' => 'bar'];
});
}
};
$result = $service->callRemember();
expect($result)->toBe(['foo' => 'bar']);
expect(Cache::has('test:key'))->toBeTrue();
});
test('cacheRemember returns cached value on second call', function () {
$service = new class
{
use CachesQuery;
public int $callCount = 0;
public function callRemember(): array
{
return $this->cacheRemember('test:counter', 60, function () {
$this->callCount++;
return ['count' => $this->callCount];
});
}
};
$first = $service->callRemember();
$second = $service->callRemember();
expect($first)->toBe(['count' => 1]);
expect($second)->toBe(['count' => 1]);
expect($service->callCount)->toBe(1);
});
test('cacheForget removes cached value', function () {
$service = new class
{
use CachesQuery;
public function callRemember(): array
{
return $this->cacheRemember('test:forget', 60, function () {
return ['data' => true];
});
}
public function callForget(): void
{
$this->cacheForget('test:forget');
}
};
$service->callRemember();
expect(Cache::has('test:forget'))->toBeTrue();
$service->callForget();
expect(Cache::has('test:forget'))->toBeFalse();
});
test('cacheForever stores value without expiration', function () {
$service = new class
{
use CachesQuery;
public function callForever(): string
{
return $this->cacheForever('test:forever', function () {
return 'permanent';
});
}
};
$result = $service->callForever();
expect($result)->toBe('permanent');
expect(Cache::has('test:forever'))->toBeTrue();
});
});
// ─── Category Cache ────────────────────────────────────────
describe('Category Caching', function () {
test('getSelectOptions caches results', function () {
Category::factory()->count(3)->create();
$service = app(CategoryService::class);
$first = $service->getSelectOptions();
expect($first)->toHaveCount(3);
expect(Cache::has('master:categories:options'))->toBeTrue();
Category::create(['name' => 'New Category']);
$second = $service->getSelectOptions();
expect($second)->toHaveCount(3);
Cache::forget('master:categories:options');
$third = $service->getSelectOptions();
expect($third)->toHaveCount(4);
});
test('create invalidates category cache', function () {
$service = app(CategoryService::class);
$service->getSelectOptions();
expect(Cache::has('master:categories:options'))->toBeTrue();
$service->create(['name' => 'Test']);
expect(Cache::has('master:categories:options'))->toBeFalse();
});
test('update invalidates category cache', function () {
$category = Category::factory()->create();
$service = app(CategoryService::class);
$service->getSelectOptions();
expect(Cache::has('master:categories:options'))->toBeTrue();
$service->update($category, ['name' => 'Updated']);
expect(Cache::has('master:categories:options'))->toBeFalse();
});
test('delete invalidates category cache', function () {
$category = Category::factory()->create();
$service = app(CategoryService::class);
$service->getSelectOptions();
expect(Cache::has('master:categories:options'))->toBeTrue();
$service->delete($category);
expect(Cache::has('master:categories:options'))->toBeFalse();
});
});
// ─── Supplier Cache ────────────────────────────────────────
describe('Supplier Caching', function () {
test('getSelectOptions caches results', function () {
Supplier::factory()->count(3)->create();
$service = app(SupplierService::class);
$first = $service->getSelectOptions();
expect($first)->toHaveCount(3);
expect(Cache::has('master:suppliers:options'))->toBeTrue();
Cache::forget('master:suppliers:options');
$second = $service->getSelectOptions();
expect($second)->toHaveCount(3);
});
test('create invalidates supplier cache', function () {
$service = app(SupplierService::class);
$service->getSelectOptions();
expect(Cache::has('master:suppliers:options'))->toBeTrue();
$service->create(['name' => 'Test Supplier', 'phone_number' => '08123', 'address' => 'Jl. Test']);
expect(Cache::has('master:suppliers:options'))->toBeFalse();
});
test('update invalidates supplier cache', function () {
$supplier = Supplier::factory()->create();
$service = app(SupplierService::class);
$service->getSelectOptions();
expect(Cache::has('master:suppliers:options'))->toBeTrue();
$service->update($supplier, ['name' => 'Updated']);
expect(Cache::has('master:suppliers:options'))->toBeFalse();
});
test('delete invalidates supplier cache', function () {
$supplier = Supplier::factory()->create();
$service = app(SupplierService::class);
$service->getSelectOptions();
expect(Cache::has('master:suppliers:options'))->toBeTrue();
$service->delete($supplier);
expect(Cache::has('master:suppliers:options'))->toBeFalse();
});
});
// ─── Customer Cache ────────────────────────────────────────
describe('Customer Caching', function () {
test('getSelectOptions caches results', function () {
Customer::factory()->count(3)->create();
$service = app(CustomerService::class);
$first = $service->getSelectOptions();
expect($first)->toHaveCount(3);
expect(Cache::has('master:customers:options'))->toBeTrue();
Cache::forget('master:customers:options');
$second = $service->getSelectOptions();
expect($second)->toHaveCount(3);
});
test('create invalidates customer cache', function () {
$service = app(CustomerService::class);
$service->getSelectOptions();
expect(Cache::has('master:customers:options'))->toBeTrue();
$service->create(['name' => 'Test Customer', 'phone_number' => '08123', 'address' => 'Jl. Test']);
expect(Cache::has('master:customers:options'))->toBeFalse();
});
test('createAndReturn invalidates customer cache', function () {
$service = app(CustomerService::class);
$service->getSelectOptions();
expect(Cache::has('master:customers:options'))->toBeTrue();
$customer = $service->createAndReturn(['name' => 'Test', 'phone_number' => '08123', 'address' => 'Jl. Test']);
expect($customer)->toBeInstanceOf(Customer::class);
expect(Cache::has('master:customers:options'))->toBeFalse();
});
test('update invalidates customer cache', function () {
$customer = Customer::factory()->create();
$service = app(CustomerService::class);
$service->getSelectOptions();
expect(Cache::has('master:customers:options'))->toBeTrue();
$service->update($customer, ['name' => 'Updated']);
expect(Cache::has('master:customers:options'))->toBeFalse();
});
test('delete invalidates customer cache', function () {
$customer = Customer::factory()->create();
$service = app(CustomerService::class);
$service->getSelectOptions();
expect(Cache::has('master:customers:options'))->toBeTrue();
$service->delete($customer);
expect(Cache::has('master:customers:options'))->toBeFalse();
});
});
// ─── Cache Key Isolation ───────────────────────────────────
describe('Cache Key Isolation', function () {
test('different services use different cache keys', function () {
Category::factory()->count(2)->create();
Supplier::factory()->count(3)->create();
app(CategoryService::class)->getSelectOptions();
app(SupplierService::class)->getSelectOptions();
expect(Cache::has('master:categories:options'))->toBeTrue();
expect(Cache::has('master:suppliers:options'))->toBeTrue();
Cache::forget('master:categories:options');
expect(Cache::has('master:categories:options'))->toBeFalse();
expect(Cache::has('master:suppliers:options'))->toBeTrue();
});
});