Compare commits
10 Commits
7df0e16914
...
0ede7280a4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ede7280a4 | ||
|
|
bfef057e6f | ||
|
|
76753fc304 | ||
|
|
de10185684 | ||
|
|
a9dd91fefd | ||
|
|
0bdbd080ab | ||
|
|
cd3dcce052 | ||
|
|
5abf7459da | ||
|
|
a4b83ed6cb | ||
|
|
a12e516b2d |
@ -72,11 +72,13 @@ public function updated(string $propertyName, mixed $value): void
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
$this->form->auth();
|
||||
|
||||
$this->sendRegistrationSuccessToast();
|
||||
$this->notifyPrivilegedUsersAboutNewMember();
|
||||
$this->redirectAfterRegistration();
|
||||
if ($this->form->auth()) {
|
||||
$this->sendRegistrationSuccessToast();
|
||||
$this->notifyPrivilegedUsersAboutNewMember();
|
||||
$this->redirectAfterRegistration();
|
||||
} else {
|
||||
$this->toast('Terjadi kesalahan saat mendaftarkan akun. Silakan hubungi admin atau coba beberapa saat lagi.', 'Kesalahan', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Livewire\Form;
|
||||
@ -92,7 +93,7 @@ public function validationAttributes(): array
|
||||
];
|
||||
}
|
||||
|
||||
public function auth(): string
|
||||
public function auth(): ?string
|
||||
{
|
||||
$this->validateAll();
|
||||
|
||||
@ -101,56 +102,66 @@ public function auth(): string
|
||||
$referralCode = ReferralCode::where('code', $this->referral_code)->first();
|
||||
|
||||
$user = null;
|
||||
DB::transaction(function () use ($tier, $maxUser, $referralCode, &$user): void {
|
||||
$user = User::create([
|
||||
'email' => $this->email,
|
||||
'username' => $this->username,
|
||||
'password' => Hash::make($this->password),
|
||||
'status' => UserStatus::ACTIVE,
|
||||
]);
|
||||
|
||||
Customer::create([
|
||||
'user_id' => $user->id,
|
||||
'name' => $this->name,
|
||||
'phone_number' => $this->phone_number,
|
||||
'gender' => $this->gender,
|
||||
]);
|
||||
|
||||
Membership::create([
|
||||
'user_id' => $user->id,
|
||||
'tier_id' => $tier->id,
|
||||
'reward_points' => 10,
|
||||
]);
|
||||
|
||||
ReferralCode::create([
|
||||
'user_id' => $user->id,
|
||||
'code' => generateReferralCode($this->username, $maxUser),
|
||||
]);
|
||||
|
||||
if ($this->referral_code) {
|
||||
ReferralUsage::create([
|
||||
'user_id' => $user->id,
|
||||
'referral_code_id' => $referralCode->id,
|
||||
try {
|
||||
DB::transaction(function () use ($tier, $maxUser, $referralCode, &$user): void {
|
||||
$user = User::create([
|
||||
'email' => $this->email,
|
||||
'username' => $this->username,
|
||||
'password' => Hash::make($this->password),
|
||||
'status' => UserStatus::ACTIVE,
|
||||
]);
|
||||
|
||||
$referralCode->increment('uses_count');
|
||||
}
|
||||
Customer::create([
|
||||
'user_id' => $user->id,
|
||||
'name' => $this->name,
|
||||
'phone_number' => $this->phone_number,
|
||||
'gender' => $this->gender,
|
||||
]);
|
||||
|
||||
PointRecord::create([
|
||||
'user_id' => $user->id,
|
||||
'description' => 'Pendaftaran',
|
||||
'change' => 10,
|
||||
'is_addition' => true,
|
||||
'type' => PointRecordType::TIER,
|
||||
Membership::create([
|
||||
'user_id' => $user->id,
|
||||
'tier_id' => $tier->id,
|
||||
'reward_points' => 10,
|
||||
]);
|
||||
|
||||
ReferralCode::create([
|
||||
'user_id' => $user->id,
|
||||
'code' => generateReferralCode($this->username, $maxUser),
|
||||
]);
|
||||
|
||||
if ($this->referral_code) {
|
||||
ReferralUsage::create([
|
||||
'user_id' => $user->id,
|
||||
'referral_code_id' => $referralCode->id,
|
||||
]);
|
||||
|
||||
$referralCode->increment('uses_count');
|
||||
}
|
||||
|
||||
PointRecord::create([
|
||||
'user_id' => $user->id,
|
||||
'description' => 'Pendaftaran',
|
||||
'change' => 10,
|
||||
'is_addition' => true,
|
||||
'type' => PointRecordType::TIER,
|
||||
]);
|
||||
|
||||
$user->assignRole('Customer');
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
});
|
||||
|
||||
return $this->name;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Registration Error: '.$e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'email' => $this->email,
|
||||
'username' => $this->username,
|
||||
]);
|
||||
|
||||
$user->assignRole('Customer');
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
});
|
||||
|
||||
return $this->name;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\Outlet;
|
||||
use App\Rules\UnsignedInteger;
|
||||
use App\Services\StockActivityLogService;
|
||||
use App\Traits\Forms\WithFormStockAdjustment;
|
||||
use App\Traits\Media\WithMediaHandler;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
@ -13,7 +14,7 @@
|
||||
|
||||
class BottleForm extends Form
|
||||
{
|
||||
use WithMediaHandler;
|
||||
use WithFormStockAdjustment, WithMediaHandler;
|
||||
|
||||
public ?Bottle $bottle = null;
|
||||
|
||||
@ -70,6 +71,7 @@ public function setBottle(Bottle $bottle): void
|
||||
$this->description = $bottle->description;
|
||||
$this->outlet_ids = $this->bottle->outlets->pluck('id')->toArray();
|
||||
$this->image = $this->mapMediaCollection($bottle->getMedia('image'));
|
||||
$this->loadStockAdjustments($bottle);
|
||||
}
|
||||
|
||||
public function store(): void
|
||||
@ -129,6 +131,8 @@ public function update(): void
|
||||
}
|
||||
}
|
||||
|
||||
$this->saveStockAdjustments($this->bottle, 'bottle');
|
||||
|
||||
$this->syncMedia($this->image, $this->bottle, 'image');
|
||||
$this->uploadMedia($this->image, $this->bottle, 'image');
|
||||
});
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Models\Perfume;
|
||||
use App\Rules\UnsignedInteger;
|
||||
use App\Services\StockActivityLogService;
|
||||
use App\Traits\Forms\WithFormStockAdjustment;
|
||||
use App\Traits\Media\WithMediaHandler;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
@ -14,7 +15,7 @@
|
||||
|
||||
class PerfumeForm extends Form
|
||||
{
|
||||
use WithMediaHandler;
|
||||
use WithFormStockAdjustment, WithMediaHandler;
|
||||
|
||||
public ?Perfume $perfume = null;
|
||||
|
||||
@ -96,6 +97,7 @@ public function setPerfume(Perfume $perfume): void
|
||||
$this->category_ids = $perfume->categories->pluck('id')->toArray();
|
||||
$this->outlet_ids = $this->perfume->outlets->pluck('id')->toArray();
|
||||
$this->image = $this->mapMediaCollection($perfume->getMedia('image'));
|
||||
$this->loadStockAdjustments($perfume);
|
||||
}
|
||||
|
||||
public function store(): void
|
||||
@ -163,6 +165,8 @@ public function update(): void
|
||||
}
|
||||
}
|
||||
|
||||
$this->saveStockAdjustments($this->perfume, 'perfume');
|
||||
|
||||
$this->syncMedia($data['image'], $this->perfume, 'image');
|
||||
$this->uploadMedia($data['image'], $this->perfume, 'image');
|
||||
});
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\Product;
|
||||
use App\Rules\UnsignedInteger;
|
||||
use App\Services\StockActivityLogService;
|
||||
use App\Traits\Forms\WithFormStockAdjustment;
|
||||
use App\Traits\Media\WithMediaHandler;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
@ -13,7 +14,7 @@
|
||||
|
||||
class ProductForm extends Form
|
||||
{
|
||||
use WithMediaHandler;
|
||||
use WithFormStockAdjustment, WithMediaHandler;
|
||||
|
||||
public ?Product $product = null;
|
||||
|
||||
@ -67,6 +68,7 @@ public function setProduct(Product $product): void
|
||||
$this->description = $product->description;
|
||||
$this->outlet_ids = $this->product->outlets->pluck('id')->toArray();
|
||||
$this->image = $this->mapMediaCollection($product->getMedia('image'));
|
||||
$this->loadStockAdjustments($product);
|
||||
}
|
||||
|
||||
public function store(): void
|
||||
@ -130,6 +132,8 @@ public function update(): void
|
||||
}
|
||||
}
|
||||
|
||||
$this->saveStockAdjustments($this->product, 'product');
|
||||
|
||||
$this->syncMedia($data['image'], $this->product, 'image');
|
||||
$this->uploadMedia($data['image'], $this->product, 'image');
|
||||
});
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
|
||||
use App\Enums\UserStatus;
|
||||
use App\Notifications\VerifyEmailQueued;
|
||||
use Dyrynda\Database\Support\CascadeSoftDeletes;
|
||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
@ -46,6 +47,11 @@ protected function casts(): array
|
||||
];
|
||||
}
|
||||
|
||||
public function sendEmailVerificationNotification()
|
||||
{
|
||||
$this->notify(new VerifyEmailQueued);
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
protected function active(Builder $query): void
|
||||
{
|
||||
|
||||
26
app/Notifications/VerifyEmailQueued.php
Normal file
26
app/Notifications/VerifyEmailQueued.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class VerifyEmailQueued extends VerifyEmail implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function toMail($notifiable): MailMessage
|
||||
{
|
||||
$verificationUrl = $this->verificationUrl($notifiable);
|
||||
|
||||
return (new MailMessage)
|
||||
->subject('Verifikasi Alamat Email')
|
||||
->greeting('Halo, '.$notifiable->customer->name.'!')
|
||||
->line('Klik tombol di bawah ini untuk memverifikasi alamat email Anda.')
|
||||
->action('Verifikasi Email', $verificationUrl)
|
||||
->line('Jika Anda tidak membuat akun, abaikan email ini.')
|
||||
->salutation('Salam, '.config('app.name'));
|
||||
}
|
||||
}
|
||||
75
app/Traits/Forms/WithFormStockAdjustment.php
Normal file
75
app/Traits/Forms/WithFormStockAdjustment.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\Forms;
|
||||
|
||||
use App\Models\Outlet;
|
||||
use App\Services\StockActivityLogService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
trait WithFormStockAdjustment
|
||||
{
|
||||
public array $stock_adjustments = [];
|
||||
|
||||
protected function loadStockAdjustments(Model $item): void
|
||||
{
|
||||
$this->stock_adjustments = [
|
||||
'outlet' => [],
|
||||
];
|
||||
|
||||
$user = auth()->user();
|
||||
$userOutletIds = $user->outlets->pluck('id')->toArray();
|
||||
|
||||
$itemOutlets = $item->outlets()
|
||||
->whereIn('outlets.id', $userOutletIds)
|
||||
->withPivot('stock')
|
||||
->get();
|
||||
|
||||
foreach ($itemOutlets as $outlet) {
|
||||
$stock = $outlet->pivot->stock ?? 0;
|
||||
$this->stock_adjustments['outlet'][$outlet->id] = [
|
||||
'name' => $outlet->name,
|
||||
'stock' => $stock,
|
||||
'new_stock' => (string) $stock,
|
||||
'note' => '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
protected function saveStockAdjustments(Model $item, string $itemType): void
|
||||
{
|
||||
$itemName = $item->name.($itemType === 'bottle' ? " ({$item->size}ml)" : '');
|
||||
|
||||
if (isset($this->stock_adjustments['outlet'])) {
|
||||
foreach ($this->stock_adjustments['outlet'] as $id => $adj) {
|
||||
$newStock = (int) str_replace(['.', ','], '', (string) ($adj['new_stock'] ?: '0'));
|
||||
$oldStock = (int) $adj['stock'];
|
||||
|
||||
if ($newStock === $oldStock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$outlet = Outlet::find($id);
|
||||
if ($outlet) {
|
||||
$item->outlets()->syncWithoutDetaching([$id => ['stock' => $newStock]]);
|
||||
|
||||
StockActivityLogService::logOutlet(
|
||||
logName: 'adjustment',
|
||||
event: 'update',
|
||||
description: "Penyesuaian manual untuk {$itemName}: berubah dari ".formatCurrencyNumber($oldStock).' menjadi '.formatCurrencyNumber($newStock).'. Catatan: '.($adj['note'] ?: '-'),
|
||||
performedOn: $item,
|
||||
outlet: $outlet,
|
||||
quantityChange: $newStock - $oldStock,
|
||||
previousStock: $oldStock,
|
||||
newStock: $newStock,
|
||||
extraProperties: [
|
||||
'note' => $adj['note'],
|
||||
'adjustment_type' => 'manual',
|
||||
'item_type' => $itemType,
|
||||
'item_id' => $item->id,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -52,6 +52,9 @@ public function run(): void
|
||||
TestimonialSeeder::class,
|
||||
ArticleSeeder::class,
|
||||
OrderSeeder::class,
|
||||
PurchaseSeeder::class,
|
||||
ExpenseSeeder::class,
|
||||
PayrollSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
30
database/seeders/ExpenseSeeder.php
Normal file
30
database/seeders/ExpenseSeeder.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Expense;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class ExpenseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$users = User::all();
|
||||
$outlets = Outlet::all();
|
||||
|
||||
if ($users->isEmpty() || $outlets->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Expense::factory()
|
||||
->count(100)
|
||||
->recycle($users)
|
||||
->recycle($outlets)
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,7 @@ public function run(): void
|
||||
'Murni' => 1.0,
|
||||
];
|
||||
|
||||
$sizes = Bottle::distinct()->pluck('size');
|
||||
$sizes = Bottle::reorder()->pluck('size');
|
||||
|
||||
foreach ($sizes as $size) {
|
||||
foreach ($ratios as $quality => $ratio) {
|
||||
|
||||
@ -48,33 +48,46 @@ public function run(): void
|
||||
$orderableItems = $orderableItems->merge($products);
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 50; $i++) {
|
||||
$outlet = $outlets->random();
|
||||
$user = $users->random();
|
||||
$customer = $customers->random();
|
||||
$startDate = now()->subMonths(6);
|
||||
$endDate = now();
|
||||
$orderCount = 0;
|
||||
|
||||
$status = $this->getRandomStatus();
|
||||
$orderedAt = now()->subDays(rand(0, 90));
|
||||
for ($date = $startDate->copy(); $date->lte($endDate); $date->addDay()) {
|
||||
$ordersPerDay = rand(1, 10);
|
||||
|
||||
$order = Order::create([
|
||||
'outlet_id' => $outlet->id,
|
||||
'user_id' => $user->id,
|
||||
'customer_id' => $customer->id,
|
||||
'voucher_id' => null,
|
||||
'invoice_number' => 'INV'.$orderedAt->format('Ymd').str_pad($i + 1, 4, '0', STR_PAD_LEFT),
|
||||
'channel' => OrderChannel::OUTLET,
|
||||
'status' => $status,
|
||||
'ordered_at' => $orderedAt,
|
||||
'created_at' => $orderedAt,
|
||||
'updated_at' => $orderedAt,
|
||||
'cogs' => 0,
|
||||
'subtotal' => 0,
|
||||
'discount' => 0,
|
||||
'voucher_discount' => 0,
|
||||
'total' => 0,
|
||||
]);
|
||||
for ($i = 0; $i < $ordersPerDay; $i++) {
|
||||
$orderCount++;
|
||||
$outlet = $outlets->random();
|
||||
$user = $users->random();
|
||||
$customer = $customers->random();
|
||||
|
||||
$this->createOrderItems($order, $user, $orderableItems);
|
||||
$status = $this->getRandomStatus();
|
||||
$orderedAt = $date->copy()->startOfDay()->addHours(rand(8, 22))->addMinutes(rand(0, 59));
|
||||
|
||||
if ($orderedAt->gt(now())) {
|
||||
$orderedAt = now();
|
||||
}
|
||||
|
||||
$order = Order::create([
|
||||
'outlet_id' => $outlet->id,
|
||||
'user_id' => $user->id,
|
||||
'customer_id' => $customer->id,
|
||||
'voucher_id' => null,
|
||||
'invoice_number' => 'INV'.$orderedAt->format('Ymd').str_pad($orderCount, 4, '0', STR_PAD_LEFT),
|
||||
'channel' => OrderChannel::OUTLET,
|
||||
'status' => $status,
|
||||
'ordered_at' => $orderedAt,
|
||||
'created_at' => $orderedAt,
|
||||
'updated_at' => $orderedAt,
|
||||
'cogs' => 0,
|
||||
'subtotal' => 0,
|
||||
'discount' => 0,
|
||||
'voucher_discount' => 0,
|
||||
'total' => 0,
|
||||
]);
|
||||
|
||||
$this->createOrderItems($order, $user, $orderableItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,7 +115,7 @@ private function createOrderItems(Order $order, User $user, Collection $orderabl
|
||||
|
||||
for ($j = 0; $j < $itemCount; $j++) {
|
||||
$item = $orderables->random();
|
||||
$qty = rand(1, 3);
|
||||
$qty = rand(20, 100);
|
||||
|
||||
$price = $item->sale_price ?? rand(50000, 150000);
|
||||
$cogs = $item->cost_price ?? (int) ($price * 0.4);
|
||||
|
||||
56
database/seeders/PayrollSeeder.php
Normal file
56
database/seeders/PayrollSeeder.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Enums\IsPaid;
|
||||
use App\Models\Payroll;
|
||||
use App\Models\PayrollAdjustment;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class PayrollSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$users = User::whereHas('roles', function ($q) {
|
||||
$q->whereIn('name', ['Admin', 'Leader']);
|
||||
})->get();
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = Carbon::now();
|
||||
|
||||
foreach ($users as $user) {
|
||||
// 12 months backward including current month
|
||||
for ($i = 0; $i < 12; $i++) {
|
||||
$month = $now->copy()->subMonths($i);
|
||||
$isCurrentMonth = $i === 0;
|
||||
|
||||
$payroll = Payroll::factory()
|
||||
->for($user)
|
||||
->withPeriodMonth($month->format('Y-m'))
|
||||
->create([
|
||||
'is_paid' => $isCurrentMonth ? IsPaid::NOT_PAID : IsPaid::PAID,
|
||||
'paid_at' => $isCurrentMonth ? null : $month->copy()->addDays(28),
|
||||
'created_at' => $month->copy()->addDays(20),
|
||||
'updated_at' => $month->copy()->addDays(20),
|
||||
]);
|
||||
|
||||
// Create some adjustments
|
||||
PayrollAdjustment::factory()
|
||||
->count(rand(0, 3))
|
||||
->create([
|
||||
'payroll_id' => $payroll->id,
|
||||
'created_at' => $month->copy()->addDays(22),
|
||||
'updated_at' => $month->copy()->addDays(22),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
111
database/seeders/PurchaseSeeder.php
Normal file
111
database/seeders/PurchaseSeeder.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Perfume;
|
||||
use App\Models\Product;
|
||||
use App\Models\Purchase;
|
||||
use App\Models\PurchaseItem;
|
||||
use App\Models\User;
|
||||
use App\Models\Warehouse;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class PurchaseSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$warehouses = Warehouse::all();
|
||||
if ($warehouses->isEmpty()) {
|
||||
$warehouses = Warehouse::factory(3)->create();
|
||||
}
|
||||
|
||||
$users = User::all();
|
||||
if ($users->isEmpty()) {
|
||||
$users = User::factory(5)->create();
|
||||
}
|
||||
|
||||
$perfumes = Perfume::all();
|
||||
$products = Product::all();
|
||||
|
||||
if ($perfumes->isEmpty() && $products->isEmpty()) {
|
||||
$perfumes = Perfume::factory(10)->create();
|
||||
}
|
||||
|
||||
$purchasableItems = collect();
|
||||
if ($perfumes->isNotEmpty()) {
|
||||
$purchasableItems = $purchasableItems->merge($perfumes);
|
||||
}
|
||||
if ($products->isNotEmpty()) {
|
||||
$purchasableItems = $purchasableItems->merge($products);
|
||||
}
|
||||
|
||||
$startDate = now()->subMonths(6)->startOfMonth();
|
||||
$endDate = now();
|
||||
$purchaseCount = 0;
|
||||
|
||||
// Iterate through each month
|
||||
for ($date = $startDate->copy(); $date->lte($endDate); $date->addMonth()) {
|
||||
// Purchases twice a month (e.g., 5th and 20th)
|
||||
$purchaseDates = [
|
||||
$date->copy()->day(5),
|
||||
$date->copy()->day(20),
|
||||
];
|
||||
|
||||
foreach ($purchaseDates as $purchaseDate) {
|
||||
if ($purchaseDate->gt($endDate)) {
|
||||
continue; // Skip if date is in the future
|
||||
}
|
||||
|
||||
$purchaseCount++;
|
||||
$warehouse = $warehouses->random();
|
||||
|
||||
$purchaseTime = $purchaseDate->copy()->addHours(rand(9, 16))->addMinutes(rand(0, 59));
|
||||
|
||||
$purchase = Purchase::create([
|
||||
'warehouse_id' => $warehouse->id,
|
||||
'invoice_number' => 'PO'.$purchaseTime->format('Ymd').str_pad($purchaseCount, 4, '0', STR_PAD_LEFT),
|
||||
'purchase_date' => $purchaseTime->toDateString(),
|
||||
'total' => 0,
|
||||
'note' => 'Restock '.$purchaseTime->format('F Y'),
|
||||
'created_at' => $purchaseTime,
|
||||
'updated_at' => $purchaseTime,
|
||||
]);
|
||||
|
||||
$this->createPurchaseItems($purchase, $users->random(), $purchasableItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createPurchaseItems(Purchase $purchase, User $user, Collection $purchasables): void
|
||||
{
|
||||
$itemCount = rand(5, 15);
|
||||
$totalPurchase = 0;
|
||||
|
||||
for ($j = 0; $j < $itemCount; $j++) {
|
||||
$item = $purchasables->random();
|
||||
$qty = rand(50, 500);
|
||||
|
||||
$unitPrice = $item->cost_price ?? (int) (($item->sale_price ?? rand(50000, 150000)) * 0.4);
|
||||
$totalPrice = $unitPrice * $qty;
|
||||
|
||||
PurchaseItem::create([
|
||||
'purchase_id' => $purchase->id,
|
||||
'user_id' => $user->id,
|
||||
'purchasable_type' => $item->getMorphClass(),
|
||||
'purchasable_id' => $item->id,
|
||||
'quantity' => $qty,
|
||||
'unit_price' => $unitPrice,
|
||||
'total_price' => $totalPrice,
|
||||
'created_at' => $purchase->created_at,
|
||||
'updated_at' => $purchase->created_at,
|
||||
]);
|
||||
|
||||
$totalPurchase += $totalPrice;
|
||||
}
|
||||
|
||||
$purchase->update([
|
||||
'total' => $totalPurchase,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -76,5 +76,89 @@ public function run(): void
|
||||
|
||||
$owner->assignRole(UserRole::Owner);
|
||||
$owner->outlets()->attach($outlet_ids);
|
||||
|
||||
// leader
|
||||
$leader = User::create([
|
||||
'email' => 'leader@gmail.com',
|
||||
'username' => 'leader',
|
||||
'password' => Hash::make(config('myconfig.password_default')),
|
||||
'status' => UserStatus::ACTIVE,
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
Employee::create([
|
||||
'user_id' => $leader->id,
|
||||
'code' => '0003',
|
||||
'full_name' => 'Leader User',
|
||||
'phone_number' => '082121495801',
|
||||
'gender' => Gender::MALE,
|
||||
'address' => 'Leader Address',
|
||||
'birthdate' => '2000-01-01',
|
||||
'hire_date' => now(),
|
||||
'status' => EmploymentStatus::PERMANENT,
|
||||
]);
|
||||
|
||||
ReferralCode::create([
|
||||
'user_id' => $leader->id,
|
||||
'code' => generateReferralCode('leader', 3),
|
||||
]);
|
||||
|
||||
$leader->assignRole(UserRole::Leader);
|
||||
$leader->outlets()->attach($outlet_ids);
|
||||
|
||||
// admin per outlet
|
||||
$outlets = Outlet::all();
|
||||
$userCounter = 4;
|
||||
foreach ($outlets as $outlet) {
|
||||
$adminUsername = 'admin_'.preg_replace('/[^a-zA-Z0-9]/', '', strtolower($outlet->name));
|
||||
$adminEmail = $adminUsername.'@gmail.com';
|
||||
|
||||
$admin = User::create([
|
||||
'email' => $adminEmail,
|
||||
'username' => $adminUsername,
|
||||
'password' => Hash::make(config('myconfig.password_default')),
|
||||
'status' => UserStatus::ACTIVE,
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
Employee::create([
|
||||
'user_id' => $admin->id,
|
||||
'code' => str_pad($userCounter, 4, '0', STR_PAD_LEFT),
|
||||
'full_name' => 'Admin '.$outlet->name,
|
||||
'phone_number' => '0821214958'.str_pad($userCounter, 2, '0', STR_PAD_LEFT),
|
||||
'gender' => Gender::MALE,
|
||||
'address' => 'Admin Address',
|
||||
'birthdate' => '2000-01-01',
|
||||
'hire_date' => now(),
|
||||
'status' => EmploymentStatus::PERMANENT,
|
||||
]);
|
||||
|
||||
ReferralCode::create([
|
||||
'user_id' => $admin->id,
|
||||
'code' => generateReferralCode($adminUsername, $userCounter),
|
||||
]);
|
||||
|
||||
$admin->assignRole(UserRole::Admin);
|
||||
$admin->outlets()->attach($outlet->id);
|
||||
|
||||
$userCounter++;
|
||||
}
|
||||
|
||||
// partner
|
||||
$partner = User::create([
|
||||
'email' => 'partner@gmail.com',
|
||||
'username' => 'partner',
|
||||
'password' => Hash::make(config('myconfig.password_default')),
|
||||
'status' => UserStatus::ACTIVE,
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
ReferralCode::create([
|
||||
'user_id' => $partner->id,
|
||||
'code' => generateReferralCode('partner', $userCounter),
|
||||
]);
|
||||
|
||||
$partner->assignRole(UserRole::Partner);
|
||||
$partner->outlets()->attach($outlet_ids);
|
||||
}
|
||||
}
|
||||
|
||||
59
resources/views/components/forms/stock-management.blade.php
Normal file
59
resources/views/components/forms/stock-management.blade.php
Normal file
@ -0,0 +1,59 @@
|
||||
@props(['form'])
|
||||
|
||||
<div class="mt-8 space-y-4">
|
||||
<div class="flex items-center gap-2 px-1">
|
||||
<flux:icon icon="map-pin" variant="outline" class="text-gray-400" size="sm" />
|
||||
<flux:heading size="lg">Manajemen Stok Outlet</flux:heading>
|
||||
</div>
|
||||
|
||||
@if (count($form->stock_adjustments['outlet'] ?? []) > 0)
|
||||
<flux:card class="space-y-6">
|
||||
<div class="flex items-center gap-2 border-b border-gray-100 dark:border-white/5 pb-4 -mx-6 px-6">
|
||||
<div class="w-1.5 h-1.5 rounded-full bg-emerald-500"></div>
|
||||
<flux:heading size="md" class="font-bold">Outlet Penjualan</flux:heading>
|
||||
</div>
|
||||
|
||||
<div class="space-y-8">
|
||||
@foreach ($form->stock_adjustments['outlet'] as $id => $adj)
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex flex-col">
|
||||
<span
|
||||
class="text-sm font-semibold text-gray-800 dark:text-gray-200">{{ $adj['name'] }}</span>
|
||||
</div>
|
||||
<flux:badge color="emerald" size="sm" variant="pill" class="font-mono">Stok:
|
||||
{{ number_format($adj['stock']) }}</flux:badge>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<flux:field>
|
||||
<flux:label class="text-xs font-medium opacity-70">Penyesuaian Stok Baru</flux:label>
|
||||
<flux:input.group>
|
||||
<flux:input.group.prefix class="bg-gray-50/50 dark:bg-white/5">
|
||||
<flux:icon icon="plus" variant="mini" />
|
||||
</flux:input.group.prefix>
|
||||
<flux:input type="number"
|
||||
wire:model="form.stock_adjustments.outlet.{{ $id }}.new_stock"
|
||||
class="font-mono" />
|
||||
</flux:input.group>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label class="text-xs font-medium opacity-70">Catatan Perubahan</flux:label>
|
||||
<flux:input placeholder="Alasan penyesuaian..."
|
||||
wire:model="form.stock_adjustments.outlet.{{ $id }}.note" autocomplete="off" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</div>
|
||||
@if (!$loop->last)
|
||||
<flux:separator variant="subtle" />
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</flux:card>
|
||||
@else
|
||||
<flux:card class="p-6 text-center italic text-gray-400 text-sm">
|
||||
Tidak ada outlet yang terhubung atau Anda tidak memiliki akses ke outlet manapun.
|
||||
</flux:card>
|
||||
@endif
|
||||
</div>
|
||||
@ -63,6 +63,10 @@ class="col-span-{{ auth()->user()->hasRole(['Developer', 'Owner'])? '1': '2' }}"
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
@if ($form->bottle?->exists)
|
||||
<x-forms.stock-management :form="$form" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -87,6 +87,10 @@ class="grid grid-cols-1 md:grid-cols-{{ auth()->user()->hasRole(['Developer', 'O
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
@if ($form->perfume?->exists)
|
||||
<x-forms.stock-management :form="$form" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -58,6 +58,10 @@ class="col-span-{{ auth()->user()->hasRole(['Developer', 'Owner'])? '1': '2' }}"
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
@if ($form->product?->exists)
|
||||
<x-forms.stock-management :form="$form" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user