diff --git a/app/Filament/Pages/EditProfile.php b/app/Filament/Pages/EditProfile.php
new file mode 100644
index 0000000..dbfeda9
--- /dev/null
+++ b/app/Filament/Pages/EditProfile.php
@@ -0,0 +1,20 @@
+components([
+ $this->getNameFormComponent(),
+ $this->getEmailFormComponent(),
+ $this->getPasswordFormComponent(),
+ $this->getPasswordConfirmationFormComponent(),
+ $this->getCurrentPasswordFormComponent(),
+ ]);
+ }
+}
diff --git a/app/Filament/Resources/Activities/RelationManagers/ParticipantsRelationManager.php b/app/Filament/Resources/Activities/RelationManagers/ParticipantsRelationManager.php
index de911d2..ebd48f2 100644
--- a/app/Filament/Resources/Activities/RelationManagers/ParticipantsRelationManager.php
+++ b/app/Filament/Resources/Activities/RelationManagers/ParticipantsRelationManager.php
@@ -59,7 +59,7 @@ class ParticipantsRelationManager extends RelationManager
if (($data['status'] ?? 'hadir') === 'hadir') {
$activity = $this->getOwnerRecord();
MemberPoint::firstOrCreate(
- ['user_id' => $data['recordId'], 'source_type' => 'activity', 'source_id' => $activity->id],
+ ['user_id' => $data['recordId'], 'source_type' => \App\Models\Activity::class, 'source_id' => $activity->id],
['points' => 10, 'reason' => "Hadir di kegiatan: {$activity->title}"]
);
}
@@ -70,7 +70,7 @@ class ParticipantsRelationManager extends RelationManager
->after(function (EditAction $action, $record, array $data) {
$activity = $this->getOwnerRecord();
$existing = MemberPoint::where('user_id', $record->id)
- ->where('source_type', 'activity')
+ ->where('source_type', \App\Models\Activity::class)
->where('source_id', $activity->id)
->first();
@@ -79,7 +79,7 @@ class ParticipantsRelationManager extends RelationManager
'user_id' => $record->id,
'points' => 10,
'reason' => "Hadir di kegiatan: {$activity->title}",
- 'source_type' => 'activity',
+ 'source_type' => \App\Models\Activity::class,
'source_id' => $activity->id,
]);
} elseif (($data['status'] ?? 'hadir') !== 'hadir' && $existing) {
@@ -90,7 +90,7 @@ class ParticipantsRelationManager extends RelationManager
->after(function ($record) {
$activity = $this->getOwnerRecord();
MemberPoint::where('user_id', $record->id)
- ->where('source_type', 'activity')
+ ->where('source_type', \App\Models\Activity::class)
->where('source_id', $activity->id)
->delete();
}),
@@ -100,7 +100,7 @@ class ParticipantsRelationManager extends RelationManager
DetachBulkAction::make()
->after(function ($records) {
$activity = $this->getOwnerRecord();
- MemberPoint::where('source_type', 'activity')
+ MemberPoint::where('source_type', \App\Models\Activity::class)
->where('source_id', $activity->id)
->whereIn('user_id', $records->pluck('id'))
->delete();
diff --git a/app/Filament/Resources/Activities/Schemas/ActivityForm.php b/app/Filament/Resources/Activities/Schemas/ActivityForm.php
index ed9321e..53e7b52 100644
--- a/app/Filament/Resources/Activities/Schemas/ActivityForm.php
+++ b/app/Filament/Resources/Activities/Schemas/ActivityForm.php
@@ -17,6 +17,8 @@ class ActivityForm
Textarea::make('description')->label('Deskripsi')->rows(3)->columnSpanFull(),
DatePicker::make('start_date')->label('Mulai')->required(),
DatePicker::make('end_date')->label('Selesai')->required(),
+ TextInput::make('budget')->label('Estimasi Budget (Rp)')->numeric()
+ ->helperText('Kosongkan jika tidak ada budget. < Rp500.000: langsung | Rp500.000–2.000.000: approval ketua | > Rp2.000.000: voting'),
DateTimePicker::make('executed_at')->label('Waktu Pelaksanaan')
->visible(fn ($record) => $record?->status === 'approved'),
Textarea::make('execution_notes')->label('Catatan Pelaksanaan')->rows(3)->columnSpanFull()
diff --git a/app/Filament/Resources/Activities/Tables/ActivitiesTable.php b/app/Filament/Resources/Activities/Tables/ActivitiesTable.php
index 3c07020..a63269a 100644
--- a/app/Filament/Resources/Activities/Tables/ActivitiesTable.php
+++ b/app/Filament/Resources/Activities/Tables/ActivitiesTable.php
@@ -52,7 +52,8 @@ class ActivitiesTable
->icon('heroicon-o-paper-airplane')
->color('info')
->requiresConfirmation()
- ->visible(fn ($record) => $record->status === 'draft')
+ ->visible(fn ($record) => $record->status === 'draft'
+ && $record->created_by === auth()->id())
->action(fn ($record) => $record->update(['status' => 'pending'])),
Action::make('approve')
->label('Setujui')
diff --git a/app/Filament/Resources/CashRecords/Schemas/CashRecordForm.php b/app/Filament/Resources/CashRecords/Schemas/CashRecordForm.php
index 9efdb0a..7a56eca 100644
--- a/app/Filament/Resources/CashRecords/Schemas/CashRecordForm.php
+++ b/app/Filament/Resources/CashRecords/Schemas/CashRecordForm.php
@@ -2,6 +2,7 @@
namespace App\Filament\Resources\CashRecords\Schemas;
+use App\Models\Activity;
use App\Models\CashCategory;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Placeholder;
@@ -27,6 +28,9 @@ class CashRecordForm
->live(),
Textarea::make('description')->label('Keterangan')->required()->columnSpanFull(),
DatePicker::make('date')->label('Tanggal')->required(),
+ Select::make('activity_id')->label('Kegiatan Terkait')
+ ->options(Activity::whereIn('status', ['approved'])->pluck('title', 'id'))
+ ->searchable()->nullable(),
]);
}
}
diff --git a/app/Filament/Resources/Divisions/Schemas/DivisionForm.php b/app/Filament/Resources/Divisions/Schemas/DivisionForm.php
index a849a1d..699e371 100644
--- a/app/Filament/Resources/Divisions/Schemas/DivisionForm.php
+++ b/app/Filament/Resources/Divisions/Schemas/DivisionForm.php
@@ -2,6 +2,8 @@
namespace App\Filament\Resources\Divisions\Schemas;
+use App\Models\User;
+use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
@@ -13,6 +15,12 @@ class DivisionForm
return $schema->components([
TextInput::make('name')->label('Nama')->required(),
Textarea::make('description')->label('Deskripsi')->rows(3)->columnSpanFull(),
+ Select::make('leader_id')->label('Penanggung Jawab')
+ ->options(
+ User::role('pengurus')->where('status', 'aktif')->pluck('name', 'id')
+ )
+ ->searchable()
+ ->nullable(),
]);
}
}
diff --git a/app/Filament/Resources/Divisions/Tables/DivisionsTable.php b/app/Filament/Resources/Divisions/Tables/DivisionsTable.php
index 96a7ad6..71f82dd 100644
--- a/app/Filament/Resources/Divisions/Tables/DivisionsTable.php
+++ b/app/Filament/Resources/Divisions/Tables/DivisionsTable.php
@@ -16,6 +16,7 @@ class DivisionsTable
->columns([
TextColumn::make('name')->label('Nama')->searchable()->sortable(),
TextColumn::make('description')->label('Deskripsi')->limit(50),
+ TextColumn::make('leader.name')->label('Penanggung Jawab')->default('-'),
TextColumn::make('members_count')->counts('members')->label('Anggota'),
])
->recordActions([EditAction::make()])
diff --git a/app/Filament/Resources/MemberPoints/MemberPointResource.php b/app/Filament/Resources/MemberPoints/MemberPointResource.php
index ccad068..adb0da9 100644
--- a/app/Filament/Resources/MemberPoints/MemberPointResource.php
+++ b/app/Filament/Resources/MemberPoints/MemberPointResource.php
@@ -21,6 +21,11 @@ class MemberPointResource extends Resource
protected static string|\UnitEnum|null $navigationGroup = 'Organisasi';
protected static ?string $navigationLabel = 'Poin Anggota';
+ public static function canAccess(): bool
+ {
+ return auth()->user()->hasRole('super_admin');
+ }
+
public static function form(Schema $schema): Schema
{
return MemberPointForm::configure($schema);
diff --git a/app/Filament/Resources/Posts/PostResource.php b/app/Filament/Resources/Posts/PostResource.php
index c5bcc67..644027d 100644
--- a/app/Filament/Resources/Posts/PostResource.php
+++ b/app/Filament/Resources/Posts/PostResource.php
@@ -18,7 +18,7 @@ class PostResource extends Resource
protected static ?string $model = Post::class;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-newspaper';
protected static string|\UnitEnum|null $navigationGroup = 'Blog';
- protected static ?string $navigationLabel = 'Post';
+ protected static ?string $navigationLabel = 'Artikel';
// Label dinamis sesuai role
public static function getModelLabel(): string
diff --git a/app/Filament/Resources/Users/Schemas/UserForm.php b/app/Filament/Resources/Users/Schemas/UserForm.php
index c5aa66d..fde2548 100644
--- a/app/Filament/Resources/Users/Schemas/UserForm.php
+++ b/app/Filament/Resources/Users/Schemas/UserForm.php
@@ -36,7 +36,25 @@ class UserForm
->columnSpanFull(),
DatePicker::make('last_activity_date')->label('Terakhir Aktif'),
Select::make('roles')->relationship('roles', 'name')
- ->multiple()->preload()->label('Role'),
+ ->multiple()->preload()->label('Role')
+ ->getOptionLabelFromRecordUsing(fn ($record) => $record->name)
+ ->afterStateHydrated(function ($component, $state) {
+ if (is_array($state)) {
+ $filtered = array_filter($state, fn ($id) => \Spatie\Permission\Models\Role::find($id)?->name !== 'anggota');
+ $component->state(array_values($filtered));
+ }
+ })
+ ->options(function () {
+ $user = auth()->user();
+ $query = \Spatie\Permission\Models\Role::query()
+ ->whereNotIn('name', ['super_admin', 'panel_user', 'anggota']);
+
+ if (! $user->can('AssignKoordinator')) {
+ $query->where('name', '!=', 'koordinator');
+ }
+
+ return $query->pluck('name', 'id');
+ }),
]);
}
}
diff --git a/app/Filament/Resources/Users/Tables/UsersTable.php b/app/Filament/Resources/Users/Tables/UsersTable.php
index b28f59c..0d66d88 100644
--- a/app/Filament/Resources/Users/Tables/UsersTable.php
+++ b/app/Filament/Resources/Users/Tables/UsersTable.php
@@ -23,7 +23,9 @@ class UsersTable
TextColumn::make('division.name')->label('Divisi')->sortable(),
TextColumn::make('status')->badge()
->color(fn ($state) => $state === 'aktif' ? 'success' : 'danger'),
- TextColumn::make('roles.name')->label('Role')->badge(),
+ TextColumn::make('roles.name')->label('Role')->badge()
+ ->getStateUsing(fn ($record) => $record->roles->pluck('name')->filter(fn ($r) => $r !== 'anggota')->values())
+ ->placeholder('-'),
])
->filters([
SelectFilter::make('status')
diff --git a/app/Http/Controllers/PublicController.php b/app/Http/Controllers/PublicController.php
index 460272b..ddc3d6d 100644
--- a/app/Http/Controllers/PublicController.php
+++ b/app/Http/Controllers/PublicController.php
@@ -63,7 +63,10 @@ class PublicController extends Controller
return view('public.kontak');
}
- public function kontakStore(\Illuminate\Http\Request $request)
+ public function guide()
+ {
+ return view('public.guide');
+ } public function kontakStore(\Illuminate\Http\Request $request)
{
$data = $request->validate([
'name' => 'required|string|max:100',
diff --git a/app/Models/Activity.php b/app/Models/Activity.php
index f4a37f3..96e5c22 100644
--- a/app/Models/Activity.php
+++ b/app/Models/Activity.php
@@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Activity extends Model
{
protected $fillable = [
- 'title', 'description', 'start_date', 'end_date',
+ 'title', 'description', 'budget', 'start_date', 'end_date',
'created_by', 'status', 'approved_by', 'approved_at',
'executed_at', 'execution_notes',
];
@@ -44,4 +44,9 @@ class Activity extends Model
return $this->belongsToMany(User::class, 'activity_member')
->withPivot('status', 'notes');
}
+
+ public function cashRecords(): \Illuminate\Database\Eloquent\Relations\HasMany
+ {
+ return $this->hasMany(CashRecord::class);
+ }
}
diff --git a/app/Models/CashRecord.php b/app/Models/CashRecord.php
index c53e270..d1bdf8b 100644
--- a/app/Models/CashRecord.php
+++ b/app/Models/CashRecord.php
@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
class CashRecord extends Model
{
protected $fillable = [
- 'amount', 'category_id', 'description',
+ 'amount', 'category_id', 'activity_id', 'description',
'date', 'created_by', 'verified_by', 'verified_at',
];
@@ -29,6 +29,11 @@ class CashRecord extends Model
return $this->belongsTo(CashCategory::class, 'category_id');
}
+ public function activity(): BelongsTo
+ {
+ return $this->belongsTo(Activity::class);
+ }
+
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
diff --git a/app/Models/Division.php b/app/Models/Division.php
index a35c6dc..87789d3 100644
--- a/app/Models/Division.php
+++ b/app/Models/Division.php
@@ -7,10 +7,15 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Division extends Model
{
- protected $fillable = ['name', 'description'];
+ protected $fillable = ['name', 'description', 'leader_id'];
public function members(): HasMany
{
return $this->hasMany(User::class);
}
+
+ public function leader(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+ {
+ return $this->belongsTo(User::class, 'leader_id');
+ }
}
diff --git a/app/Models/User.php b/app/Models/User.php
index 5de451b..aace0aa 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -67,7 +67,7 @@ class User extends Authenticatable implements FilamentUser
public function canAccessPanel(Panel $panel): bool
{
- return true;
+ return $this->status === 'aktif';
}
public function canImpersonate(): bool
diff --git a/app/Models/Vote.php b/app/Models/Vote.php
index 0a255a3..507ba6c 100644
--- a/app/Models/Vote.php
+++ b/app/Models/Vote.php
@@ -10,7 +10,7 @@ class Vote extends Model
{
protected $fillable = [
'title', 'description', 'type',
- 'related_id', 'status', 'deadline', 'created_by',
+ 'related_id', 'related_type', 'status', 'deadline', 'created_by',
];
protected $casts = [
diff --git a/app/Observers/ActivityObserver.php b/app/Observers/ActivityObserver.php
index 691eb87..67952dc 100644
--- a/app/Observers/ActivityObserver.php
+++ b/app/Observers/ActivityObserver.php
@@ -4,6 +4,8 @@ namespace App\Observers;
use App\Models\Activity;
use App\Models\ActivityLog;
+use App\Models\Approval;
+use App\Models\Vote;
use App\Services\NotificationService;
use Filament\Notifications\Notification;
use Illuminate\Support\Facades\Auth;
@@ -30,13 +32,6 @@ class ActivityObserver
return;
}
- if ($new === 'approved' && $activity->wasChanged('executed_at') && empty($activity->execution_notes)) {
- Notification::make()->title('Catatan pelaksanaan wajib diisi')
- ->danger()->send();
- $activity->executed_at = null;
- return;
- }
-
ActivityLog::create([
'user_id' => Auth::id(),
'action' => 'status_changed',
@@ -49,6 +44,33 @@ class ActivityObserver
NotificationService::toRole('ketua', 'Kegiatan Menunggu Persetujuan',
"Kegiatan \"{$activity->title}\" diajukan untuk disetujui.", 'warning',
route('filament.admin.resources.activities.edit', $activity));
+
+ // Threshold budget
+ $budget = $activity->budget;
+ if ($budget !== null && $budget >= 500_000 && $budget <= 2_000_000) {
+ Approval::firstOrCreate(
+ ['model_type' => Activity::class, 'model_id' => $activity->id],
+ ['required_approvals' => 1, 'status' => 'pending']
+ );
+ } elseif ($budget !== null && $budget > 2_000_000) {
+ $exists = Vote::where('related_type', Activity::class)
+ ->where('related_id', $activity->id)
+ ->where('type', 'finance')
+ ->exists();
+
+ if (! $exists) {
+ Vote::create([
+ 'title' => "Persetujuan Budget Kegiatan: {$activity->title}",
+ 'description' => "Budget kegiatan senilai Rp " . number_format($budget, 0, ',', '.') . " memerlukan persetujuan voting.",
+ 'type' => 'finance',
+ 'related_id' => $activity->id,
+ 'related_type' => Activity::class,
+ 'status' => 'open',
+ 'deadline' => now()->addDays(3),
+ 'created_by' => Auth::id() ?? $activity->created_by,
+ ]);
+ }
+ }
}
if (in_array($new, ['approved', 'rejected']) && $activity->creator) {
diff --git a/app/Observers/CashRecordObserver.php b/app/Observers/CashRecordObserver.php
index 0302af8..043f515 100644
--- a/app/Observers/CashRecordObserver.php
+++ b/app/Observers/CashRecordObserver.php
@@ -23,7 +23,7 @@ class CashRecordObserver
// Threshold: 500rb–2jt → buat approval ketua + notif
if ($record->amount >= 500_000 && $record->amount <= 2_000_000) {
- $approval = Approval::create([
+ Approval::create([
'model_type' => CashRecord::class,
'model_id' => $record->id,
'required_approvals' => 1,
@@ -41,13 +41,14 @@ class CashRecordObserver
// Threshold: > 2jt → buat voting + notif semua anggota
if ($record->amount > 2_000_000) {
Vote::create([
- 'title' => "Persetujuan Transaksi: {$record->description}",
- 'description' => "Transaksi senilai Rp " . number_format($record->amount, 0, ',', '.') . " memerlukan persetujuan voting.",
- 'type' => 'finance',
- 'related_id' => $record->id,
- 'status' => 'open',
- 'deadline' => now()->addDays(3),
- 'created_by' => Auth::id() ?? $record->created_by,
+ 'title' => "Persetujuan Transaksi: {$record->description}",
+ 'description' => "Transaksi senilai Rp " . number_format($record->amount, 0, ',', '.') . " memerlukan persetujuan voting.",
+ 'type' => 'finance',
+ 'related_id' => $record->id,
+ 'related_type' => CashRecord::class,
+ 'status' => 'open',
+ 'deadline' => now()->addDays(3),
+ 'created_by' => Auth::id() ?? $record->created_by,
]);
NotificationService::toRole('ketua',
diff --git a/app/Observers/PostObserver.php b/app/Observers/PostObserver.php
index 55d1fe1..eec7375 100644
--- a/app/Observers/PostObserver.php
+++ b/app/Observers/PostObserver.php
@@ -9,7 +9,7 @@ class PostObserver
{
public function updated(Post $post): void
{
- if ($post->wasChanged('status') && $post->status === 'approved') {
+ if ($post->wasChanged('status') && $post->status === 'published') {
MemberPoint::firstOrCreate(
[
'user_id' => $post->author_id,
diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php
index f224712..3b11022 100644
--- a/app/Observers/UserObserver.php
+++ b/app/Observers/UserObserver.php
@@ -12,6 +12,11 @@ class UserObserver
{
public function updated(User $user): void
{
+ // Pastikan role anggota selalu ada
+ if (! $user->hasRole('anggota')) {
+ $user->assignRole('anggota');
+ }
+
// Log perubahan status anggota
if ($user->wasChanged('status')) {
MemberStatusLog::create([
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 72104b9..90bc862 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -12,6 +12,9 @@ use App\Observers\CashRecordObserver;
use App\Observers\PostObserver;
use App\Observers\UserObserver;
use App\Observers\VoteObserver;
+use Filament\Support\Facades\FilamentView;
+use Filament\View\PanelsRenderHook;
+use Illuminate\Support\HtmlString;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@@ -23,5 +26,19 @@ class AppServiceProvider extends ServiceProvider
Activity::observe(ActivityObserver::class);
Vote::observe(VoteObserver::class);
Post::observe(PostObserver::class);
+
+ FilamentView::registerRenderHook(
+ PanelsRenderHook::TOPBAR_LOGO_AFTER,
+ fn () => new HtmlString(
+ '
+
+ '
+ )
+ );
}
}
diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php
index c81556a..3f96bf7 100644
--- a/app/Providers/Filament/AdminPanelProvider.php
+++ b/app/Providers/Filament/AdminPanelProvider.php
@@ -7,7 +7,6 @@ use BezhanSalleh\FilamentShield\FilamentShieldPlugin;
use Filament\Http\Middleware\AuthenticateSession;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
-use Filament\Navigation\NavigationItem;
use Filament\Pages\Dashboard;
use Filament\Panel;
use Filament\PanelProvider;
@@ -33,6 +32,7 @@ class AdminPanelProvider extends PanelProvider
->path('dashboard')
->viteTheme('resources/css/filament/admin/theme.css')
->login()
+ ->profile(\App\Filament\Pages\EditProfile::class)
->colors([
'primary' => Color::Amber,
])
@@ -51,12 +51,7 @@ class AdminPanelProvider extends PanelProvider
'Konten',
'Organisasi',
])
- ->navigationItems([
- NavigationItem::make('Website Publik')
- ->url('/', shouldOpenInNewTab: true)
- ->icon('heroicon-o-globe-alt')
- ->sort(99),
- ])
+
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets')
->widgets([
AccountWidget::class,
diff --git a/app/Services/NotificationService.php b/app/Services/NotificationService.php
index 93fc9d0..73ac3bd 100644
--- a/app/Services/NotificationService.php
+++ b/app/Services/NotificationService.php
@@ -36,6 +36,6 @@ class NotificationService
public static function toAll(string $title, string $body, string $color = 'info', ?string $url = null): void
{
- self::send(User::all(), $title, $body, $color, $url);
+ self::send(User::where('status', 'aktif')->get(), $title, $body, $color, $url);
}
}
diff --git a/config/filament-shield.php b/config/filament-shield.php
index 9e7c472..8ce5475 100644
--- a/config/filament-shield.php
+++ b/config/filament-shield.php
@@ -233,8 +233,9 @@ return [
*/
'custom_permissions' => [
- 'ViewDraft:Activity', // Lihat kegiatan berstatus draft milik user lain (hanya super_admin)
- 'Publish:Post', // Publish / unpublish artikel (editor)
+ 'ViewDraft:Activity', // Lihat kegiatan berstatus draft milik user lain (hanya super_admin)
+ 'Publish:Post', // Publish / unpublish artikel (editor)
+ 'AssignKoordinator', // Assign/cabut role koordinator ke anggota (hanya ketua)
],
/*
diff --git a/database/migrations/2026_04_05_152811_add_leader_id_to_divisions_table.php b/database/migrations/2026_04_05_152811_add_leader_id_to_divisions_table.php
new file mode 100644
index 0000000..d33a2ca
--- /dev/null
+++ b/database/migrations/2026_04_05_152811_add_leader_id_to_divisions_table.php
@@ -0,0 +1,23 @@
+foreignId('leader_id')->nullable()->constrained('users')->nullOnDelete()->after('description');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('divisions', function (Blueprint $table) {
+ $table->dropForeignIdFor(\App\Models\User::class, 'leader_id');
+ $table->dropColumn('leader_id');
+ });
+ }
+};
diff --git a/database/migrations/2026_04_05_161652_add_budget_to_activities_and_activity_id_to_cash_records.php b/database/migrations/2026_04_05_161652_add_budget_to_activities_and_activity_id_to_cash_records.php
new file mode 100644
index 0000000..19b03d8
--- /dev/null
+++ b/database/migrations/2026_04_05_161652_add_budget_to_activities_and_activity_id_to_cash_records.php
@@ -0,0 +1,31 @@
+unsignedBigInteger('budget')->nullable()->after('description');
+ });
+
+ Schema::table('cash_records', function (Blueprint $table) {
+ $table->foreignId('activity_id')->nullable()->constrained('activities')->nullOnDelete()->after('category_id');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('cash_records', function (Blueprint $table) {
+ $table->dropForeignIdFor(\App\Models\Activity::class, 'activity_id');
+ $table->dropColumn('activity_id');
+ });
+
+ Schema::table('activities', function (Blueprint $table) {
+ $table->dropColumn('budget');
+ });
+ }
+};
diff --git a/database/migrations/2026_04_05_162107_add_related_type_to_votes_table.php b/database/migrations/2026_04_05_162107_add_related_type_to_votes_table.php
new file mode 100644
index 0000000..7a842ac
--- /dev/null
+++ b/database/migrations/2026_04_05_162107_add_related_type_to_votes_table.php
@@ -0,0 +1,22 @@
+string('related_type')->nullable()->after('related_id');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('votes', function (Blueprint $table) {
+ $table->dropColumn('related_type');
+ });
+ }
+};
diff --git a/database/seeders/DivisionSeeder.php b/database/seeders/DivisionSeeder.php
index 84c02b6..5f85f95 100644
--- a/database/seeders/DivisionSeeder.php
+++ b/database/seeders/DivisionSeeder.php
@@ -15,6 +15,7 @@ class DivisionSeeder extends Seeder
['name' => 'Olahraga', 'description' => 'Bidang olahraga dan kesehatan'],
['name' => 'Seni & Budaya', 'description' => 'Bidang seni dan pelestarian budaya'],
['name' => 'Lingkungan', 'description' => 'Bidang lingkungan hidup'],
+ ['name' => 'Teknologi Informasi', 'description' => 'Bidang teknologi dan informasi digital'],
];
foreach ($divisions as $division) {
diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php
index abef586..d20eb6c 100644
--- a/database/seeders/PermissionSeeder.php
+++ b/database/seeders/PermissionSeeder.php
@@ -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', 'editor'] as $role) {
+ foreach (['super_admin', 'ketua', 'bendahara', 'pengurus', 'anggota', 'auditor', 'editor', 'koordinator'] as $role) {
Role::firstOrCreate(['name' => $role, 'guard_name' => 'web']);
}
@@ -29,12 +29,25 @@ class PermissionSeeder extends Seeder
$anggota = Role::findByName('anggota');
$auditor = Role::findByName('auditor');
$editor = Role::findByName('editor');
+ $koordinator = Role::findByName('koordinator');
$ketua->syncPermissions(Permission::where('name', 'not like', '%Role%')
->where('name', 'not like', '%Permission%')
->where('name', '!=', 'ViewDraft:Activity')
->get());
+ // Pastikan ketua punya AssignKoordinator
+ if ($p = Permission::where('name', 'AssignKoordinator')->first()) {
+ $ketua->givePermissionTo($p);
+ }
+
+ $koordinator->syncPermissions(Permission::whereIn('name', [
+ 'ViewAny:Activity', 'View:Activity', 'Create:Activity', 'Update:Activity', 'Delete:Activity',
+ 'ViewAny:Vote', 'View:Vote',
+ 'ViewAny:Post', 'View:Post', 'Create:Post', 'Update:Post', 'Delete:Post',
+ 'ViewAny:MemberPoint', 'View:MemberPoint',
+ ])->get());
+
$bendahara->syncPermissions(Permission::where('name', 'like', '%CashRecord%')
->orWhere('name', 'like', '%CashCategory%')
->orWhere('name', 'like', '%MemberDue%')
diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php
index 5f2a2d5..8a1d950 100644
--- a/database/seeders/UserSeeder.php
+++ b/database/seeders/UserSeeder.php
@@ -2,7 +2,6 @@
namespace Database\Seeders;
-use App\Models\Division;
use App\Models\User;
use Illuminate\Database\Seeder;
@@ -10,9 +9,9 @@ class UserSeeder extends Seeder
{
public function run(): void
{
- $divisions = Division::pluck('id')->toArray();
+ $divisions = \App\Models\Division::all();
- // super_admin
+ // super_admin (tanpa divisi)
User::factory()->createOne([
'name' => 'Super Admin',
'email' => 'admin@admin.com',
@@ -21,22 +20,29 @@ class UserSeeder extends Seeder
'status' => 'aktif',
])->assignRole('super_admin');
- // 2 user per role
- foreach (['ketua', 'bendahara', 'pengurus', 'auditor', 'anggota'] as $role) {
- User::factory(2)->create(['division_id' => fake()->randomElement($divisions)])
- ->each(fn ($user) => $user->assignRole($role));
+ // ketua, bendahara, auditor — tanpa divisi spesifik
+ foreach (['ketua', 'bendahara', 'auditor'] as $role) {
+ User::factory(2)->create()->each(fn ($u) => $u->assignRole($role));
}
// 1 editor
User::factory()->createOne([
- 'name' => 'Editor Konten',
- 'email' => 'editor@persegi.test',
- 'password' => bcrypt('password'),
- 'status' => 'aktif',
- 'division_id' => fake()->randomElement($divisions),
+ 'name' => 'Editor Konten',
+ 'email' => 'editor@persegi.test',
+ 'password' => bcrypt('password'),
+ 'status' => 'aktif',
])->assignRole('editor');
- // 2 user tanpa role
- User::factory(2)->create(['division_id' => fake()->randomElement($divisions)]);
+ // Setiap divisi: 1 pengurus (jadi leader) + 3–8 anggota
+ foreach ($divisions as $division) {
+ $pengurus = User::factory()->create(['division_id' => $division->id]);
+ $pengurus->assignRole('pengurus');
+
+ $division->update(['leader_id' => $pengurus->id]);
+
+ $count = rand(3, 8);
+ User::factory($count)->create(['division_id' => $division->id])
+ ->each(fn ($u) => $u->assignRole('anggota'));
+ }
}
}
diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
new file mode 100644
index 0000000..381e32c
--- /dev/null
+++ b/docs/USER_GUIDE.md
@@ -0,0 +1,230 @@
+# Panduan Penggunaan Sistem Persegi
+
+Sistem manajemen internal Organisasi Pemuda Desa Karangdadap.
+Akses panel: **https://persegi.nyawiji.net/admin**
+
+---
+
+## Daftar Isi
+
+1. [Login & Akses Panel](#1-login--akses-panel)
+2. [Dashboard](#2-dashboard)
+3. [Manajemen Anggota](#3-manajemen-anggota)
+4. [Divisi](#4-divisi)
+5. [Kegiatan](#5-kegiatan)
+6. [Keuangan (Kas)](#6-keuangan-kas)
+7. [Iuran Anggota](#7-iuran-anggota)
+8. [Voting](#8-voting)
+9. [Approval](#9-approval)
+10. [Audit Internal](#10-audit-internal)
+11. [Konten & Blog](#11-konten--blog)
+12. [Poin Anggota](#12-poin-anggota)
+13. [Notifikasi](#13-notifikasi)
+14. [Hak Akses per Role](#14-hak-akses-per-role)
+
+---
+
+## 1. Login & Akses Panel
+
+1. Buka **https://persegi.nyawiji.net/admin**
+2. Masukkan email dan password yang diberikan pengurus
+3. Klik **Masuk**
+
+> Hanya anggota dengan status **aktif** yang bisa login. Jika tidak bisa masuk, hubungi pengurus.
+
+---
+
+## 2. Dashboard
+
+Setelah login, halaman utama menampilkan:
+- **Statistik** — jumlah anggota aktif, kegiatan, kas masuk/keluar
+- **Log Aktivitas** — perubahan terbaru di sistem
+- **Leaderboard Poin** — 10 anggota dengan poin tertinggi
+
+---
+
+## 3. Manajemen Anggota
+
+**Menu:** Organisasi → Anggota
+
+### Tambah Anggota Baru
+1. Klik tombol **Tambah Anggota**
+2. Isi nama, email, nomor telepon, alamat, dan divisi
+3. Atur status: **Aktif** atau **Nonaktif**
+4. Klik **Simpan**
+
+> Anggota baru otomatis mendapat role `anggota` dan bisa login ke panel.
+
+### Nonaktifkan Anggota
+1. Buka halaman edit anggota
+2. Ubah status ke **Nonaktif**
+3. Isi alasan nonaktif
+4. Klik **Simpan**
+
+> Anggota nonaktif tidak bisa login ke panel.
+
+### Assign Role Tambahan
+Role bisa ditambahkan di field **Role** saat edit anggota.
+- `koordinator` — hanya bisa di-assign oleh **ketua**
+- Role lain (`pengurus`, `bendahara`, dll) — bisa di-assign oleh yang punya akses
+
+---
+
+## 4. Divisi
+
+**Menu:** Organisasi → Divisi
+
+- Tambah, edit, atau hapus divisi
+- Setiap divisi bisa memiliki **Penanggung Jawab** — dipilih dari anggota dengan role `pengurus`
+
+---
+
+## 5. Kegiatan
+
+**Menu:** Kegiatan → Kegiatan
+
+### Alur Status Kegiatan
+
+```
+Draft → Pending (diajukan) → Approved / Rejected
+```
+
+### Buat Kegiatan Baru (Pengurus / Koordinator)
+1. Klik **Tambah Kegiatan**
+2. Isi judul, deskripsi, tanggal mulai & selesai
+3. Isi **Estimasi Budget** jika ada (opsional)
+4. Klik **Simpan** — kegiatan tersimpan sebagai **Draft**
+
+### Ajukan Kegiatan
+1. Buka kegiatan yang sudah dibuat
+2. Ubah status ke **Pending**
+3. Klik **Simpan**
+
+> Ketua akan mendapat notifikasi untuk menyetujui.
+> Jika budget ≥ Rp500.000, akan dibuat **Approval** otomatis.
+> Jika budget > Rp2.000.000, akan dibuat **Voting** otomatis.
+
+### Setujui / Tolak Kegiatan (Ketua)
+1. Buka kegiatan dengan status **Pending**
+2. Ubah status ke **Approved** atau **Rejected**
+3. Klik **Simpan**
+
+### Catat Kehadiran Peserta
+1. Buka kegiatan yang sudah **Approved**
+2. Buka tab **Kehadiran Peserta**
+3. Klik **Tambah Peserta** → pilih anggota → atur status kehadiran
+4. Anggota yang hadir otomatis mendapat **+10 poin**
+
+---
+
+## 6. Keuangan (Kas)
+
+**Menu:** Keuangan → Transaksi
+
+### Catat Transaksi Baru (Bendahara)
+1. Klik **Tambah Transaksi**
+2. Pilih kategori, isi jumlah, keterangan, dan tanggal
+3. Pilih **Kegiatan Terkait** jika transaksi untuk kegiatan tertentu (opsional)
+4. Klik **Simpan**
+
+### Alur Verifikasi Otomatis
+
+| Jumlah | Alur |
+|---|---|
+| < Rp500.000 | Bisa langsung diverifikasi |
+| Rp500.000 – Rp2.000.000 | Approval ketua diperlukan |
+| > Rp2.000.000 | Voting anggota diperlukan |
+
+### Verifikasi Transaksi (Ketua)
+1. Buka transaksi yang menunggu verifikasi
+2. Klik tombol **Verifikasi**
+
+> Transaksi yang sudah diverifikasi **tidak bisa diubah atau dihapus**.
+
+---
+
+## 7. Iuran Anggota
+
+**Menu:** Organisasi → Iuran Anggota
+
+- Catat iuran per anggota per periode (format: `YYYY-MM`, contoh: `2026-04`)
+- Status iuran: **Lunas** atau **Belum Lunas**
+
+---
+
+## 8. Voting
+
+**Menu:** Keputusan → Voting
+
+- Voting dibuat otomatis oleh sistem saat ada transaksi atau budget kegiatan > Rp2.000.000
+- Anggota bisa melihat dan memilih suara di halaman detail voting
+- Voting otomatis tertutup setelah deadline
+
+---
+
+## 9. Approval
+
+**Menu:** Keputusan → Approval
+
+- Approval dibuat otomatis untuk transaksi atau budget kegiatan Rp500.000–Rp2.000.000
+- Ketua bisa menyetujui atau menolak di halaman detail approval
+
+---
+
+## 10. Audit Internal
+
+**Menu:** Audit → Temuan Audit
+
+- Auditor bisa membuat temuan audit
+- Pengurus terkait bisa memberikan respons terhadap temuan
+
+---
+
+## 11. Konten & Blog
+
+**Menu:** Konten → Post
+
+### Buat Artikel
+1. Klik **Tambah Post**
+2. Isi judul, konten, dan slug
+3. Simpan sebagai **Draft** atau langsung **Publish**
+
+> Artikel yang dipublish otomatis memberikan **+5 poin** ke penulis.
+> Editor bisa me-review dan publish/unpublish artikel.
+
+---
+
+## 12. Poin Anggota
+
+**Menu:** Organisasi → Poin Anggota
+
+Poin diberikan otomatis:
+| Aktivitas | Poin |
+|---|---|
+| Hadir di kegiatan | +10 |
+| Artikel dipublish | +5 |
+
+---
+
+## 13. Notifikasi
+
+Ikon lonceng di pojok kanan atas menampilkan notifikasi masuk, seperti:
+- Kegiatan menunggu persetujuan
+- Status kegiatan diubah
+- Transaksi butuh approval/voting
+- Status keanggotaan diubah
+
+---
+
+## 14. Hak Akses per Role
+
+| Role | Yang Bisa Dilakukan |
+|---|---|
+| `super_admin` | Semua akses |
+| `ketua` | Approve kegiatan, verifikasi kas, lihat semua data |
+| `bendahara` | Input kas & iuran |
+| `pengurus` | Buat & ajukan kegiatan, lihat anggota & divisi |
+| `koordinator` | Buat & kelola kegiatan milik sendiri (sebelum disetujui) |
+| `anggota` | Lihat kegiatan, voting, poin, buat artikel |
+| `auditor` | Lihat semua data + buat temuan audit |
+| `editor` | Review & publish artikel |
diff --git a/public/images/logo.png b/public/images/logo.png
index 87dbe56..9720b78 100644
Binary files a/public/images/logo.png and b/public/images/logo.png differ
diff --git a/resources/views/public/guide.blade.php b/resources/views/public/guide.blade.php
new file mode 100644
index 0000000..7e7091b
--- /dev/null
+++ b/resources/views/public/guide.blade.php
@@ -0,0 +1,282 @@
+@extends('public.layout')
+@section('title', 'Panduan Penggunaan')
+@section('content')
+
+
+
+
+
+ Panduan
+
+
Panduan
Penggunaan
+
Panduan lengkap penggunaan sistem manajemen internal Persegi.
+
+
+
+
+ {{-- Sidebar navigasi --}}
+
+
+ {{-- Konten --}}
+
+
+ {{-- Login --}}
+
+ 1. Login & Akses Panel
+
+ - Buka {{ config('app.url') }}/admin
+ - Masukkan email dan password yang diberikan pengurus
+ - Klik Masuk
+
+
+ Hanya anggota dengan status aktif yang bisa login. Jika tidak bisa masuk, hubungi pengurus.
+
+
+
+ {{-- Dashboard --}}
+
+ 2. Dashboard
+ Setelah login, halaman utama menampilkan:
+
+ - Statistik — jumlah anggota aktif, kegiatan, kas masuk/keluar
+ - Log Aktivitas — perubahan terbaru di sistem
+ - Leaderboard Poin — 10 anggota dengan poin tertinggi
+
+
+
+ {{-- Anggota --}}
+
+ 3. Manajemen Anggota
+ Menu: Organisasi → Anggota
+
+ Tambah Anggota Baru
+
+ - Klik tombol Tambah Anggota
+ - Isi nama, email, nomor telepon, alamat, dan divisi
+ - Atur status: Aktif atau Nonaktif
+ - Klik Simpan
+
+
+ Nonaktifkan Anggota
+
+ - Buka halaman edit anggota
+ - Ubah status ke Nonaktif dan isi alasan
+ - Klik Simpan
+
+
+ Anggota nonaktif tidak bisa login ke panel.
+
+
+
+ {{-- Divisi --}}
+
+ 4. Divisi
+ Menu: Organisasi → Divisi
+
+ - Tambah, edit, atau hapus divisi
+ - Setiap divisi bisa memiliki Penanggung Jawab — dipilih dari anggota dengan role pengurus
+
+
+
+ {{-- Kegiatan --}}
+
+ 5. Kegiatan
+ Menu: Kegiatan → Kegiatan
+
+
+ Draft
+ →
+ Pending
+ →
+ Approved
+ /
+ Rejected
+
+
+ Buat Kegiatan Baru
+
+ - Klik Tambah Kegiatan
+ - Isi judul, deskripsi, tanggal, dan estimasi budget (opsional)
+ - Klik Simpan — tersimpan sebagai Draft
+ - Ubah status ke Pending untuk mengajukan ke ketua
+
+
+
+ Budget ≥ Rp500.000 → approval ketua otomatis dibuat.
+ Budget > Rp2.000.000 → voting otomatis dibuat.
+
+
+ Catat Kehadiran Peserta
+
+ - Buka kegiatan yang sudah Approved
+ - Buka tab Kehadiran Peserta
+ - Tambah peserta dan atur status kehadiran
+ - Anggota yang hadir otomatis mendapat +10 poin
+
+
+
+ {{-- Kas --}}
+
+ 6. Keuangan (Kas)
+ Menu: Keuangan → Transaksi
+
+ Catat Transaksi
+
+ - Klik Tambah Transaksi
+ - Pilih kategori, isi jumlah, keterangan, dan tanggal
+ - Pilih kegiatan terkait jika ada (opsional)
+ - Klik Simpan
+
+
+
+
+
+
+ | Jumlah |
+ Alur |
+
+
+
+ | < Rp500.000 | Langsung diverifikasi |
+ | Rp500.000 – Rp2.000.000 | Perlu approval ketua |
+ | > Rp2.000.000 | Perlu voting anggota |
+
+
+
+
+
+ {{-- Iuran --}}
+
+ 7. Iuran Anggota
+ Menu: Organisasi → Iuran Anggota
+
+ - Catat iuran per anggota per periode (format:
YYYY-MM, contoh: 2026-04)
+ - Status: Lunas atau Belum Lunas
+
+
+
+ {{-- Voting --}}
+
+ 8. Voting
+ Menu: Keputusan → Voting
+
+ - Voting dibuat otomatis saat transaksi atau budget kegiatan > Rp2.000.000
+ - Anggota bisa memilih suara di halaman detail voting
+ - Voting tertutup otomatis setelah deadline
+
+
+
+ {{-- Approval --}}
+
+ 9. Approval
+ Menu: Keputusan → Approval
+
+ - Approval dibuat otomatis untuk transaksi atau budget Rp500.000–Rp2.000.000
+ - Ketua bisa menyetujui atau menolak di halaman detail approval
+
+
+
+ {{-- Audit --}}
+
+ 10. Audit Internal
+ Menu: Audit → Temuan Audit
+
+ - Auditor bisa membuat temuan audit
+ - Pengurus terkait bisa memberikan respons terhadap temuan
+
+
+
+ {{-- Konten --}}
+
+ 11. Konten & Blog
+ Menu: Konten → Post
+
+ - Klik Tambah Post
+ - Isi judul, konten, dan slug
+ - Simpan sebagai Draft atau langsung Publish
+
+
+ Artikel yang dipublish otomatis memberikan +5 poin ke penulis.
+
+
+
+ {{-- Poin --}}
+
+ 12. Poin Anggota
+ Menu: Organisasi → Poin Anggota
+
+
+
+ | Aktivitas |
+ Poin |
+
+
+
+ | Hadir di kegiatan | +10 |
+ | Artikel dipublish | +5 |
+
+
+
+
+ {{-- Notifikasi --}}
+
+ 13. Notifikasi
+ Ikon lonceng di pojok kanan atas menampilkan notifikasi masuk:
+
+ - Kegiatan menunggu persetujuan
+ - Status kegiatan diubah
+ - Transaksi butuh approval atau voting
+ - Status keanggotaan diubah
+
+
+
+ {{-- Role --}}
+
+ 14. Hak Akses per Role
+
+
+
+
+ | Role |
+ Yang Bisa Dilakukan |
+
+
+
+ | ketua | Approve kegiatan, verifikasi kas, lihat semua data |
+ | bendahara | Input kas & iuran |
+ | pengurus | Buat & ajukan kegiatan, lihat anggota & divisi |
+ | koordinator | Buat & kelola kegiatan milik sendiri (sebelum disetujui) |
+ | anggota | Lihat kegiatan, voting, poin, buat artikel |
+ | auditor | Lihat semua data + buat temuan audit |
+ | editor | Review & publish artikel |
+
+
+
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/public/layout.blade.php b/resources/views/public/layout.blade.php
index 348114e..eff9f39 100644
--- a/resources/views/public/layout.blade.php
+++ b/resources/views/public/layout.blade.php
@@ -64,11 +64,12 @@
diff --git a/routes/web.php b/routes/web.php
index 73e417f..198bbbe 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -10,3 +10,4 @@ Route::get('/blog', [\App\Http\Controllers\PublicController::class, 'blog'])->na
Route::get('/blog/{post:slug}', [\App\Http\Controllers\PublicController::class, 'blogDetail'])->name('blog.detail');
Route::get('/kontak', [\App\Http\Controllers\PublicController::class, 'kontak'])->name('kontak');
Route::post('/kontak', [\App\Http\Controllers\PublicController::class, 'kontakStore'])->name('kontak.store');
+Route::get('/panduan', [\App\Http\Controllers\PublicController::class, 'guide'])->name('guide');