Merge branch 'dev'

This commit is contained in:
2026-04-07 13:33:49 +07:00
36 changed files with 787 additions and 57 deletions
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Pages;
use Filament\Schemas\Schema;
class EditProfile extends \Filament\Auth\Pages\EditProfile
{
public function form(Schema $schema): Schema
{
return $schema
->components([
$this->getNameFormComponent(),
$this->getEmailFormComponent(),
$this->getPasswordFormComponent(),
$this->getPasswordConfirmationFormComponent(),
$this->getCurrentPasswordFormComponent(),
]);
}
}
@@ -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();
@@ -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.0002.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()
@@ -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')
@@ -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(),
]);
}
}
@@ -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(),
]);
}
}
@@ -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()])
@@ -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);
@@ -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
@@ -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');
}),
]);
}
}
@@ -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')
+4 -1
View File
@@ -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',
+6 -1
View File
@@ -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);
}
}
+6 -1
View File
@@ -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');
+6 -1
View File
@@ -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');
}
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 = [
+29 -7
View File
@@ -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) {
+2 -1
View File
@@ -23,7 +23,7 @@ class CashRecordObserver
// Threshold: 500rb2jt → 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,
@@ -45,6 +45,7 @@ class CashRecordObserver
'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,
+1 -1
View File
@@ -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,
+5
View File
@@ -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([
+17
View File
@@ -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(
'<a href="/" target="_blank" title="Website Publik"
style="display:flex;align-items:center;color:#9ca3af;margin-left:1rem;padding-left:1rem;border-left:1px solid #9ca3af"
onmouseover="this.style.color=\'#4b5563\'" onmouseout="this.style.color=\'#9ca3af\'">
<svg xmlns="http://www.w3.org/2000/svg" style="height:2rem" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
</svg>
</a>'
)
);
}
}
@@ -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,
+1 -1
View File
@@ -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);
}
}
+1
View File
@@ -235,6 +235,7 @@ return [
'custom_permissions' => [
'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)
],
/*
@@ -0,0 +1,23 @@
<?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('divisions', function (Blueprint $table) {
$table->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');
});
}
};
@@ -0,0 +1,31 @@
<?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('activities', function (Blueprint $table) {
$table->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');
});
}
};
@@ -0,0 +1,22 @@
<?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('votes', function (Blueprint $table) {
$table->string('related_type')->nullable()->after('related_id');
});
}
public function down(): void
{
Schema::table('votes', function (Blueprint $table) {
$table->dropColumn('related_type');
});
}
};
+1
View File
@@ -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) {
+14 -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', '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%')
+16 -10
View File
@@ -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,10 +20,9 @@ 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
@@ -33,10 +31,18 @@ class UserSeeder extends Seeder
'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)]);
// Setiap divisi: 1 pengurus (jadi leader) + 38 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'));
}
}
}
+230
View File
@@ -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.000Rp2.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 |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

+282
View File
@@ -0,0 +1,282 @@
@extends('public.layout')
@section('title', 'Panduan Penggunaan')
@section('content')
<div class="max-w-4xl mx-auto px-6 py-24">
<div class="mb-12">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4 flex items-center gap-2">
<span class="w-4 h-px bg-gray-400 inline-block"></span> Panduan
</p>
<h1 class="text-5xl font-bold leading-tight mb-4">Panduan<br>Penggunaan</h1>
<p class="text-gray-500">Panduan lengkap penggunaan sistem manajemen internal Persegi.</p>
</div>
<div class="grid md:grid-cols-4 gap-8">
{{-- Sidebar navigasi --}}
<aside class="md:col-span-1">
<nav class="sticky top-8 space-y-1 text-sm">
@foreach([
'#login' => 'Login & Akses',
'#dashboard' => 'Dashboard',
'#anggota' => 'Anggota',
'#divisi' => 'Divisi',
'#kegiatan' => 'Kegiatan',
'#kas' => 'Keuangan',
'#iuran' => 'Iuran',
'#voting' => 'Voting',
'#approval' => 'Approval',
'#audit' => 'Audit',
'#konten' => 'Konten & Blog',
'#poin' => 'Poin',
'#notifikasi' => 'Notifikasi',
'#role' => 'Hak Akses',
] as $href => $label)
<a href="{{ $href }}" class="block text-gray-500 hover:text-gray-900 py-1 transition">{{ $label }}</a>
@endforeach
</nav>
</aside>
{{-- Konten --}}
<main class="md:col-span-3 space-y-16 text-gray-700 leading-relaxed">
{{-- Login --}}
<section id="login">
<h2 class="text-2xl font-bold text-gray-900 mb-4">1. Login & Akses Panel</h2>
<ol class="list-decimal list-inside space-y-2 text-sm">
<li>Buka <a href="{{ config('app.url') }}/admin" class="underline text-gray-900" target="_blank">{{ config('app.url') }}/admin</a></li>
<li>Masukkan email dan password yang diberikan pengurus</li>
<li>Klik <strong>Masuk</strong></li>
</ol>
<p class="mt-3 text-sm text-gray-500 bg-gray-50 border border-gray-200 rounded-xl px-4 py-3">
Hanya anggota dengan status <strong>aktif</strong> yang bisa login. Jika tidak bisa masuk, hubungi pengurus.
</p>
</section>
{{-- Dashboard --}}
<section id="dashboard">
<h2 class="text-2xl font-bold text-gray-900 mb-4">2. Dashboard</h2>
<p class="text-sm mb-3">Setelah login, halaman utama menampilkan:</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><strong>Statistik</strong> jumlah anggota aktif, kegiatan, kas masuk/keluar</li>
<li><strong>Log Aktivitas</strong> perubahan terbaru di sistem</li>
<li><strong>Leaderboard Poin</strong> 10 anggota dengan poin tertinggi</li>
</ul>
</section>
{{-- Anggota --}}
<section id="anggota">
<h2 class="text-2xl font-bold text-gray-900 mb-4">3. Manajemen Anggota</h2>
<p class="text-xs text-gray-400 uppercase tracking-widest mb-4">Menu: Organisasi Anggota</p>
<h3 class="font-semibold text-gray-900 mb-2">Tambah Anggota Baru</h3>
<ol class="list-decimal list-inside space-y-1 text-sm mb-6">
<li>Klik tombol <strong>Tambah Anggota</strong></li>
<li>Isi nama, email, nomor telepon, alamat, dan divisi</li>
<li>Atur status: <strong>Aktif</strong> atau <strong>Nonaktif</strong></li>
<li>Klik <strong>Simpan</strong></li>
</ol>
<h3 class="font-semibold text-gray-900 mb-2">Nonaktifkan Anggota</h3>
<ol class="list-decimal list-inside space-y-1 text-sm mb-4">
<li>Buka halaman edit anggota</li>
<li>Ubah status ke <strong>Nonaktif</strong> dan isi alasan</li>
<li>Klik <strong>Simpan</strong></li>
</ol>
<p class="text-sm text-gray-500 bg-gray-50 border border-gray-200 rounded-xl px-4 py-3">
Anggota nonaktif tidak bisa login ke panel.
</p>
</section>
{{-- Divisi --}}
<section id="divisi">
<h2 class="text-2xl font-bold text-gray-900 mb-4">4. Divisi</h2>
<p class="text-xs text-gray-400 uppercase tracking-widest mb-4">Menu: Organisasi Divisi</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>Tambah, edit, atau hapus divisi</li>
<li>Setiap divisi bisa memiliki <strong>Penanggung Jawab</strong> dipilih dari anggota dengan role pengurus</li>
</ul>
</section>
{{-- Kegiatan --}}
<section id="kegiatan">
<h2 class="text-2xl font-bold text-gray-900 mb-4">5. Kegiatan</h2>
<p class="text-xs text-gray-400 uppercase tracking-widest mb-4">Menu: Kegiatan Kegiatan</p>
<div class="flex items-center gap-2 text-sm mb-6">
<span class="bg-gray-100 px-3 py-1 rounded-full">Draft</span>
<span class="text-gray-400"></span>
<span class="bg-gray-100 px-3 py-1 rounded-full">Pending</span>
<span class="text-gray-400"></span>
<span class="bg-green-100 text-green-700 px-3 py-1 rounded-full">Approved</span>
<span class="text-gray-400">/</span>
<span class="bg-red-100 text-red-700 px-3 py-1 rounded-full">Rejected</span>
</div>
<h3 class="font-semibold text-gray-900 mb-2">Buat Kegiatan Baru</h3>
<ol class="list-decimal list-inside space-y-1 text-sm mb-6">
<li>Klik <strong>Tambah Kegiatan</strong></li>
<li>Isi judul, deskripsi, tanggal, dan estimasi budget (opsional)</li>
<li>Klik <strong>Simpan</strong> tersimpan sebagai <strong>Draft</strong></li>
<li>Ubah status ke <strong>Pending</strong> untuk mengajukan ke ketua</li>
</ol>
<p class="text-sm text-gray-500 bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 mb-6">
Budget Rp500.000 approval ketua otomatis dibuat.<br>
Budget > Rp2.000.000 voting otomatis dibuat.
</p>
<h3 class="font-semibold text-gray-900 mb-2">Catat Kehadiran Peserta</h3>
<ol class="list-decimal list-inside space-y-1 text-sm">
<li>Buka kegiatan yang sudah <strong>Approved</strong></li>
<li>Buka tab <strong>Kehadiran Peserta</strong></li>
<li>Tambah peserta dan atur status kehadiran</li>
<li>Anggota yang hadir otomatis mendapat <strong>+10 poin</strong></li>
</ol>
</section>
{{-- Kas --}}
<section id="kas">
<h2 class="text-2xl font-bold text-gray-900 mb-4">6. Keuangan (Kas)</h2>
<p class="text-xs text-gray-400 uppercase tracking-widest mb-4">Menu: Keuangan Transaksi</p>
<h3 class="font-semibold text-gray-900 mb-2">Catat Transaksi</h3>
<ol class="list-decimal list-inside space-y-1 text-sm mb-6">
<li>Klik <strong>Tambah Transaksi</strong></li>
<li>Pilih kategori, isi jumlah, keterangan, dan tanggal</li>
<li>Pilih kegiatan terkait jika ada (opsional)</li>
<li>Klik <strong>Simpan</strong></li>
</ol>
<div class="overflow-x-auto">
<table class="w-full text-sm border border-gray-200 rounded-xl overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="text-left px-4 py-3 font-medium text-gray-500">Jumlah</th>
<th class="text-left px-4 py-3 font-medium text-gray-500">Alur</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr><td class="px-4 py-3">< Rp500.000</td><td class="px-4 py-3">Langsung diverifikasi</td></tr>
<tr><td class="px-4 py-3">Rp500.000 Rp2.000.000</td><td class="px-4 py-3">Perlu approval ketua</td></tr>
<tr><td class="px-4 py-3">> Rp2.000.000</td><td class="px-4 py-3">Perlu voting anggota</td></tr>
</tbody>
</table>
</div>
</section>
{{-- Iuran --}}
<section id="iuran">
<h2 class="text-2xl font-bold text-gray-900 mb-4">7. Iuran Anggota</h2>
<p class="text-xs text-gray-400 uppercase tracking-widest mb-4">Menu: Organisasi Iuran Anggota</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>Catat iuran per anggota per periode (format: <code class="bg-gray-100 px-1 rounded">YYYY-MM</code>, contoh: <code class="bg-gray-100 px-1 rounded">2026-04</code>)</li>
<li>Status: <strong>Lunas</strong> atau <strong>Belum Lunas</strong></li>
</ul>
</section>
{{-- Voting --}}
<section id="voting">
<h2 class="text-2xl font-bold text-gray-900 mb-4">8. Voting</h2>
<p class="text-xs text-gray-400 uppercase tracking-widest mb-4">Menu: Keputusan Voting</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>Voting dibuat otomatis saat transaksi atau budget kegiatan > Rp2.000.000</li>
<li>Anggota bisa memilih suara di halaman detail voting</li>
<li>Voting tertutup otomatis setelah deadline</li>
</ul>
</section>
{{-- Approval --}}
<section id="approval">
<h2 class="text-2xl font-bold text-gray-900 mb-4">9. Approval</h2>
<p class="text-xs text-gray-400 uppercase tracking-widest mb-4">Menu: Keputusan Approval</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>Approval dibuat otomatis untuk transaksi atau budget Rp500.000Rp2.000.000</li>
<li>Ketua bisa menyetujui atau menolak di halaman detail approval</li>
</ul>
</section>
{{-- Audit --}}
<section id="audit">
<h2 class="text-2xl font-bold text-gray-900 mb-4">10. Audit Internal</h2>
<p class="text-xs text-gray-400 uppercase tracking-widest mb-4">Menu: Audit Temuan Audit</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>Auditor bisa membuat temuan audit</li>
<li>Pengurus terkait bisa memberikan respons terhadap temuan</li>
</ul>
</section>
{{-- Konten --}}
<section id="konten">
<h2 class="text-2xl font-bold text-gray-900 mb-4">11. Konten & Blog</h2>
<p class="text-xs text-gray-400 uppercase tracking-widest mb-4">Menu: Konten Post</p>
<ol class="list-decimal list-inside space-y-1 text-sm mb-4">
<li>Klik <strong>Tambah Post</strong></li>
<li>Isi judul, konten, dan slug</li>
<li>Simpan sebagai <strong>Draft</strong> atau langsung <strong>Publish</strong></li>
</ol>
<p class="text-sm text-gray-500 bg-gray-50 border border-gray-200 rounded-xl px-4 py-3">
Artikel yang dipublish otomatis memberikan <strong>+5 poin</strong> ke penulis.
</p>
</section>
{{-- Poin --}}
<section id="poin">
<h2 class="text-2xl font-bold text-gray-900 mb-4">12. Poin Anggota</h2>
<p class="text-xs text-gray-400 uppercase tracking-widest mb-4">Menu: Organisasi Poin Anggota</p>
<table class="w-full text-sm border border-gray-200 rounded-xl overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="text-left px-4 py-3 font-medium text-gray-500">Aktivitas</th>
<th class="text-left px-4 py-3 font-medium text-gray-500">Poin</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr><td class="px-4 py-3">Hadir di kegiatan</td><td class="px-4 py-3 font-semibold text-green-600">+10</td></tr>
<tr><td class="px-4 py-3">Artikel dipublish</td><td class="px-4 py-3 font-semibold text-green-600">+5</td></tr>
</tbody>
</table>
</section>
{{-- Notifikasi --}}
<section id="notifikasi">
<h2 class="text-2xl font-bold text-gray-900 mb-4">13. Notifikasi</h2>
<p class="text-sm mb-3">Ikon lonceng di pojok kanan atas menampilkan notifikasi masuk:</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>Kegiatan menunggu persetujuan</li>
<li>Status kegiatan diubah</li>
<li>Transaksi butuh approval atau voting</li>
<li>Status keanggotaan diubah</li>
</ul>
</section>
{{-- Role --}}
<section id="role">
<h2 class="text-2xl font-bold text-gray-900 mb-4">14. Hak Akses per Role</h2>
<div class="overflow-x-auto">
<table class="w-full text-sm border border-gray-200 rounded-xl overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="text-left px-4 py-3 font-medium text-gray-500">Role</th>
<th class="text-left px-4 py-3 font-medium text-gray-500">Yang Bisa Dilakukan</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 text-sm">
<tr><td class="px-4 py-3 font-mono text-xs">ketua</td><td class="px-4 py-3">Approve kegiatan, verifikasi kas, lihat semua data</td></tr>
<tr><td class="px-4 py-3 font-mono text-xs">bendahara</td><td class="px-4 py-3">Input kas & iuran</td></tr>
<tr><td class="px-4 py-3 font-mono text-xs">pengurus</td><td class="px-4 py-3">Buat & ajukan kegiatan, lihat anggota & divisi</td></tr>
<tr><td class="px-4 py-3 font-mono text-xs">koordinator</td><td class="px-4 py-3">Buat & kelola kegiatan milik sendiri (sebelum disetujui)</td></tr>
<tr><td class="px-4 py-3 font-mono text-xs">anggota</td><td class="px-4 py-3">Lihat kegiatan, voting, poin, buat artikel</td></tr>
<tr><td class="px-4 py-3 font-mono text-xs">auditor</td><td class="px-4 py-3">Lihat semua data + buat temuan audit</td></tr>
<tr><td class="px-4 py-3 font-mono text-xs">editor</td><td class="px-4 py-3">Review & publish artikel</td></tr>
</tbody>
</table>
</div>
</section>
</main>
</div>
</div>
@endsection
+2 -1
View File
@@ -64,11 +64,12 @@
<footer class="border-t border-gray-100 mt-24">
<div class="max-w-6xl mx-auto px-6 py-8 flex flex-col md:flex-row items-center justify-between gap-4">
<p class="text-sm text-gray-400">
Copyright © {{ date('Y') }} Persegi, Desa Karangdadap, Kalibagor, Banyumas
Copyright © {{ date('Y') }} Persegi, Desa Karangdadap
</p>
<div class="flex items-center gap-4 text-gray-400">
<a href="{{ route('kontak') }}" class="hover:text-gray-700 text-sm transition">Kontak</a>
<a href="{{ route('tentang') }}" class="hover:text-gray-700 text-sm transition">Tentang</a>
<a href="{{ route('guide') }}" class="hover:text-gray-700 text-sm transition">Panduan</a>
</div>
</div>
</footer>
+1
View File
@@ -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');