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

+
    +
  1. Buka {{ config('app.url') }}/admin
  2. +
  3. Masukkan email dan password yang diberikan pengurus
  4. +
  5. Klik Masuk
  6. +
+

+ 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

+
    +
  1. Klik tombol Tambah Anggota
  2. +
  3. Isi nama, email, nomor telepon, alamat, dan divisi
  4. +
  5. Atur status: Aktif atau Nonaktif
  6. +
  7. Klik Simpan
  8. +
+ +

Nonaktifkan Anggota

+
    +
  1. Buka halaman edit anggota
  2. +
  3. Ubah status ke Nonaktif dan isi alasan
  4. +
  5. Klik Simpan
  6. +
+

+ 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

+
    +
  1. Klik Tambah Kegiatan
  2. +
  3. Isi judul, deskripsi, tanggal, dan estimasi budget (opsional)
  4. +
  5. Klik Simpan — tersimpan sebagai Draft
  6. +
  7. Ubah status ke Pending untuk mengajukan ke ketua
  8. +
+ +

+ Budget ≥ Rp500.000 → approval ketua otomatis dibuat.
+ Budget > Rp2.000.000 → voting otomatis dibuat. +

+ +

Catat Kehadiran Peserta

+
    +
  1. Buka kegiatan yang sudah Approved
  2. +
  3. Buka tab Kehadiran Peserta
  4. +
  5. Tambah peserta dan atur status kehadiran
  6. +
  7. Anggota yang hadir otomatis mendapat +10 poin
  8. +
+
+ + {{-- Kas --}} +
+

6. Keuangan (Kas)

+

Menu: Keuangan → Transaksi

+ +

Catat Transaksi

+
    +
  1. Klik Tambah Transaksi
  2. +
  3. Pilih kategori, isi jumlah, keterangan, dan tanggal
  4. +
  5. Pilih kegiatan terkait jika ada (opsional)
  6. +
  7. Klik Simpan
  8. +
+ +
+ + + + + + + + + + + + +
JumlahAlur
< Rp500.000Langsung diverifikasi
Rp500.000 – Rp2.000.000Perlu approval ketua
> Rp2.000.000Perlu 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

+
    +
  1. Klik Tambah Post
  2. +
  3. Isi judul, konten, dan slug
  4. +
  5. Simpan sebagai Draft atau langsung Publish
  6. +
+

+ Artikel yang dipublish otomatis memberikan +5 poin ke penulis. +

+
+ + {{-- Poin --}} +
+

12. Poin Anggota

+

Menu: Organisasi → Poin Anggota

+ + + + + + + + + + + +
AktivitasPoin
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

+
+ + + + + + + + + + + + + + + + +
RoleYang Bisa Dilakukan
ketuaApprove kegiatan, verifikasi kas, lihat semua data
bendaharaInput kas & iuran
pengurusBuat & ajukan kegiatan, lihat anggota & divisi
koordinatorBuat & kelola kegiatan milik sendiri (sebelum disetujui)
anggotaLihat kegiatan, voting, poin, buat artikel
auditorLihat semua data + buat temuan audit
editorReview & 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');