feat: tambah role editor, workflow post, leaderboard, rekap kehadiran, kategori kas dengan type, seeder lengkap

This commit is contained in:
2026-04-05 06:21:16 +07:00
parent cde63da358
commit 6c23cc8660
40 changed files with 2432 additions and 129 deletions
@@ -10,7 +10,7 @@ return new class extends Migration
{
Schema::create('cash_categories', function (Blueprint $table) {
$table->id();
$table->string('name'); // pemasukan / pengeluaran
$table->string('name'); // nama bebas, tipe dikontrol via kolom type
$table->timestamps();
});
}
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('cash_categories', function (Blueprint $table) {
$table->enum('type', ['pemasukan', 'pengeluaran'])->after('name')->default('pemasukan');
});
// Migrate existing data
DB::table('cash_categories')->where('name', 'pengeluaran')->update(['type' => 'pengeluaran']);
DB::table('cash_categories')->where('name', '!=', 'pengeluaran')->update(['type' => 'pemasukan']);
}
public function down(): void
{
Schema::table('cash_categories', function (Blueprint $table) {
$table->dropColumn('type');
});
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
// Ubah enum status tambah 'approved'
$table->enum('status', ['draft', 'pending', 'approved', 'published', 'rejected'])
->default('draft')->change();
$table->foreignId('approved_by')->nullable()->constrained('users')->after('reviewed_by');
$table->timestamp('approved_at')->nullable()->after('approved_by');
});
}
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropForeign(['approved_by']);
$table->dropColumn(['approved_by', 'approved_at']);
$table->enum('status', ['draft', 'pending', 'published', 'rejected'])
->default('draft')->change();
});
}
};
+10 -5
View File
@@ -47,11 +47,16 @@ class ActivitySeeder extends Seeder
foreach ($activities as $data) {
$activity = Activity::create(array_merge($data, ['created_by' => $pengurus?->id]));
// Attach peserta dengan status kehadiran
$syncData = $anggota->mapWithKeys(fn ($user) => [
$user->id => ['status' => 'hadir', 'notes' => null]
])->toArray();
$activity->participants()->sync($syncData);
// Hanya kegiatan yang sudah executed yang punya data kehadiran
if (! empty($data['executed_at'])) {
$syncData = $anggota->mapWithKeys(fn ($user, $i) => [
$user->id => [
'status' => $i % 4 === 0 ? 'izin' : 'hadir',
'notes' => null,
]
])->toArray();
$activity->participants()->sync($syncData);
}
}
}
}
+27 -7
View File
@@ -2,6 +2,7 @@
namespace Database\Seeders;
use App\Models\Approval;
use App\Models\CashCategory;
use App\Models\CashRecord;
use App\Models\User;
@@ -11,8 +12,8 @@ class CashSeeder extends Seeder
{
public function run(): void
{
$pemasukan = CashCategory::firstOrCreate(['name' => 'pemasukan']);
$pengeluaran = CashCategory::firstOrCreate(['name' => 'pengeluaran']);
$pemasukan = CashCategory::firstOrCreate(['name' => 'Iuran & Donasi'], ['type' => 'pemasukan']);
$pengeluaran = CashCategory::firstOrCreate(['name' => 'Operasional'], ['type' => 'pengeluaran']);
$bendahara = User::role('bendahara')->first();
$ketua = User::role('ketua')->first();
@@ -26,11 +27,30 @@ class CashSeeder extends Seeder
];
foreach ($records as $data) {
CashRecord::create(array_merge($data, [
'created_by' => $bendahara?->id,
'verified_by' => $ketua?->id,
'verified_at' => now()->subDays(1),
]));
$amount = $data['amount'];
// 500rb2jt: butuh approval ketua, belum verified
if ($amount >= 500_000 && $amount <= 2_000_000) {
$record = CashRecord::create(array_merge($data, [
'created_by' => $bendahara?->id,
'verified_by' => null,
'verified_at' => null,
]));
Approval::create([
'model_type' => CashRecord::class,
'model_id' => $record->id,
'required_approvals' => 1,
'status' => 'pending',
]);
} else {
// < 500rb: langsung verified
CashRecord::create(array_merge($data, [
'created_by' => $bendahara?->id,
'verified_by' => $ketua?->id,
'verified_at' => now()->subDays(1),
]));
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Database\Seeders;
use App\Models\ContactMessage;
use Illuminate\Database\Seeder;
class ContactMessageSeeder extends Seeder
{
public function run(): void
{
$messages = [
['name' => 'Budi Santoso', 'email' => 'budi@example.com', 'phone' => '08123456789', 'subject' => 'Pendaftaran anggota baru', 'message' => 'Saya ingin bergabung dengan organisasi Persegi. Bagaimana cara mendaftarnya?'],
['name' => 'Siti Rahayu', 'email' => null, 'phone' => '08987654321', 'subject' => 'Informasi kegiatan kerja bakti', 'message' => 'Apakah masyarakat umum boleh ikut kegiatan kerja bakti yang akan datang?'],
['name' => 'Ahmad Fauzi', 'email' => 'ahmad@example.com', 'phone' => null, 'subject' => 'Donasi untuk kegiatan', 'message' => 'Saya ingin berdonasi untuk mendukung kegiatan. Kemana saya bisa menghubungi bendahara?'],
['name' => 'Dewi Lestari', 'email' => 'dewi@example.com', 'phone' => '08111222333', 'subject' => 'Jadwal rapat bulanan', 'message' => 'Kapan jadwal rapat bulanan berikutnya? Saya ingin hadir sebagai tamu.'],
['name' => 'Hendra Wijaya', 'email' => null, 'phone' => '08555666777', 'subject' => 'Laporan kerusakan fasilitas desa', 'message' => 'Ada lampu jalan yang mati di RT 03. Apakah Persegi bisa membantu melaporkan ke desa?'],
['name' => 'Rina Kusuma', 'email' => 'rina@example.com', 'phone' => '08222333444', 'subject' => 'Kerjasama kegiatan sosial', 'message' => 'Kami dari karang taruna RT 05 ingin mengajak kerjasama untuk kegiatan sosial bulan depan.'],
['name' => 'Joko Pramono', 'email' => null, 'phone' => '08444555666', 'subject' => 'Pertanyaan iuran anggota', 'message' => 'Berapa besaran iuran bulanan anggota Persegi? Saya tertarik untuk bergabung.'],
['name' => 'Nurul Hidayah', 'email' => 'nurul@example.com', 'phone' => '08777888999', 'subject' => 'Saran program kerja', 'message' => 'Saya punya usulan program pelatihan digital untuk pemuda desa. Bagaimana cara menyampaikannya?'],
];
foreach ($messages as $data) {
ContactMessage::create($data);
}
}
}
+2
View File
@@ -18,6 +18,8 @@ class DatabaseSeeder extends Seeder
VoteSeeder::class,
PostSeeder::class,
AuditSeeder::class,
MemberDueSeeder::class,
ContactMessageSeeder::class,
MemberPointSeeder::class,
]);
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Database\Seeders;
use App\Models\MemberDue;
use App\Models\User;
use Illuminate\Database\Seeder;
class MemberDueSeeder extends Seeder
{
public function run(): void
{
$bendahara = User::role('bendahara')->first();
$members = User::whereDoesntHave('roles', fn ($q) => $q->whereIn('name', ['super_admin', 'bendahara']))
->get();
$periods = [
now()->subMonths(3)->format('Y-m'),
now()->subMonths(2)->format('Y-m'),
now()->subMonth()->format('Y-m'),
now()->format('Y-m'),
];
foreach ($members as $i => $user) {
foreach ($periods as $j => $period) {
MemberDue::firstOrCreate(
['user_id' => $user->id, 'period' => $period],
[
'amount' => 10000,
'status' => ($i + $j) % 3 === 0 ? 'belum' : 'lunas',
'created_by' => $bendahara?->id,
]
);
}
}
}
}
+5 -5
View File
@@ -16,17 +16,17 @@ class MemberPointSeeder extends Seeder
Activity::whereNotNull('executed_at')->each(function ($activity) {
$activity->participants()->wherePivot('status', 'hadir')->each(function ($user) use ($activity) {
MemberPoint::firstOrCreate(
['user_id' => $user->id, 'source_type' => 'activity', 'source_id' => $activity->id],
['user_id' => $user->id, 'source_type' => Activity::class, 'source_id' => $activity->id],
['points' => 10, 'reason' => "Hadir di kegiatan: {$activity->title}"]
);
});
});
// Poin dari artikel yang sudah published
Post::where('status', 'published')->each(function ($post) {
// Poin dari artikel yang sudah approved
Post::where('status', 'approved')->orWhere('status', 'published')->each(function ($post) {
MemberPoint::firstOrCreate(
['user_id' => $post->author_id, 'source_type' => 'post', 'source_id' => $post->id],
['points' => 5, 'reason' => "Artikel dipublikasi: {$post->title}"]
['user_id' => $post->author_id, 'source_type' => Post::class, 'source_id' => $post->id],
['points' => 5, 'reason' => "Artikel disetujui: {$post->title}"]
);
});
}
+8 -1
View File
@@ -13,7 +13,7 @@ class PermissionSeeder extends Seeder
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
// Buat roles jika belum ada
foreach (['super_admin', 'ketua', 'bendahara', 'pengurus', 'anggota', 'auditor'] as $role) {
foreach (['super_admin', 'ketua', 'bendahara', 'pengurus', 'anggota', 'auditor', 'editor'] as $role) {
Role::firstOrCreate(['name' => $role, 'guard_name' => 'web']);
}
@@ -28,6 +28,7 @@ class PermissionSeeder extends Seeder
$pengurus = Role::findByName('pengurus');
$anggota = Role::findByName('anggota');
$auditor = Role::findByName('auditor');
$editor = Role::findByName('editor');
$ketua->syncPermissions(Permission::where('name', 'not like', '%Role%')
->where('name', 'not like', '%Permission%')
@@ -63,5 +64,11 @@ class PermissionSeeder extends Seeder
->orWhere('name', 'like', 'View:%')
->orWhere('name', 'like', '%Audit%')
->get());
$editor->syncPermissions(Permission::whereIn('name', [
'ViewAny:Post', 'View:Post', 'Create:Post', 'Update:Post', 'Delete:Post',
'DeleteAny:Post', 'ViewAny:Activity', 'View:Activity',
'Publish:Post',
])->get());
}
}
+52 -3
View File
@@ -5,38 +5,87 @@ namespace Database\Seeders;
use App\Models\Post;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
class PostSeeder extends Seeder
{
public function run(): void
{
$author = User::role('ketua')->first() ?? User::first();
$editor = User::role('editor')->first();
$anggota = User::role('anggota')->first();
$ketua = User::role('ketua')->first();
$posts = [
// Published — sudah melalui full workflow
[
'title' => 'Selamat Datang di Website Persegi',
'category' => 'pengumuman',
'content' => '<p>Kami dengan bangga mempersembahkan website resmi organisasi Persegi. Melalui website ini, masyarakat dapat mengikuti perkembangan kegiatan dan informasi terbaru dari organisasi kami.</p>',
'author_id' => $ketua?->id,
'status' => 'published',
'approved_by' => $editor?->id,
'approved_at' => now()->subDays(11),
'reviewed_by' => $editor?->id,
'published_at' => now()->subDays(10),
],
[
'title' => 'Rekrutmen Anggota Baru 2026',
'category' => 'pengumuman',
'content' => '<p>Persegi membuka pendaftaran anggota baru untuk periode 2026. Bagi pemuda Desa Karangdadap yang ingin bergabung, silakan hubungi pengurus melalui kontak yang tersedia.</p><p>Pendaftaran dibuka hingga akhir bulan April 2026.</p>',
'author_id' => $ketua?->id,
'status' => 'published',
'approved_by' => $editor?->id,
'approved_at' => now()->subDays(6),
'reviewed_by' => $editor?->id,
'published_at' => now()->subDays(5),
],
[
'title' => 'Laporan Kegiatan Kerja Bakti Desa',
'category' => 'berita',
'content' => '<p>Kegiatan kerja bakti yang dilaksanakan pada bulan lalu berjalan dengan lancar. Sebanyak 30 anggota turut berpartisipasi dalam membersihkan lingkungan desa.</p><p>Terima kasih kepada seluruh anggota yang telah berkontribusi.</p>',
'author_id' => $anggota?->id,
'status' => 'published',
'approved_by' => $editor?->id,
'approved_at' => now()->subDays(3),
'reviewed_by' => $editor?->id,
'published_at' => now()->subDays(2),
],
// Approved — sudah disetujui editor, belum diterbitkan
[
'title' => 'Jadwal Rapat Bulanan April 2026',
'category' => 'pengumuman',
'content' => '<p>Rapat bulanan akan dilaksanakan pada akhir April 2026. Seluruh anggota diharapkan hadir.</p>',
'author_id' => $anggota?->id,
'status' => 'approved',
'approved_by' => $editor?->id,
'approved_at' => now()->subDay(),
'reviewed_by' => $editor?->id,
'published_at' => null,
],
// Pending — menunggu review editor
[
'title' => 'Kegiatan Sosial Ramadan 2026',
'category' => 'berita',
'content' => '<p>Persegi berencana mengadakan kegiatan sosial berbagi sembako selama bulan Ramadan.</p>',
'author_id' => $anggota?->id,
'status' => 'pending',
'published_at' => null,
],
// Draft — belum diajukan
[
'title' => 'Profil Divisi Olahraga',
'category' => 'umum',
'content' => '<p>Divisi olahraga Persegi aktif mengadakan kegiatan rutin setiap minggu.</p>',
'author_id' => $anggota?->id,
'status' => 'draft',
'published_at' => null,
],
];
foreach ($posts as $data) {
Post::firstOrCreate(
['slug' => \Illuminate\Support\Str::slug($data['title'])],
array_merge($data, ['author_id' => $author->id, 'status' => 'published'])
['slug' => Str::slug($data['title'])],
$data
);
}
}
+9
View File
@@ -27,6 +27,15 @@ class UserSeeder extends Seeder
->each(fn ($user) => $user->assignRole($role));
}
// 1 editor
User::factory()->createOne([
'name' => 'Editor Konten',
'email' => 'editor@persegi.test',
'password' => bcrypt('password'),
'status' => 'aktif',
'division_id' => fake()->randomElement($divisions),
])->assignRole('editor');
// 2 user tanpa role
User::factory(2)->create(['division_id' => fake()->randomElement($divisions)]);
}