feat: tambah role editor, workflow post, leaderboard, rekap kehadiran, kategori kas dengan type, seeder lengkap
This commit is contained in:
+2
-1
@@ -34,7 +34,8 @@ DB_PASSWORD=
|
||||
Lalu jalankan:
|
||||
```bash
|
||||
php artisan migrate --seed
|
||||
php artisan shield:generate --panel=admin
|
||||
php artisan shield:generate --panel=admin --all -n
|
||||
php artisan db:seed --class=PermissionSeeder --force
|
||||
php artisan shield:super-admin --user=1
|
||||
php artisan serve
|
||||
```
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Sistem web production-ready untuk **Organisasi Pemuda Desa Persegi**, berlokasi di Desa Karangdadap, Kecamatan Kalibagor, Kabupaten Banyumas.
|
||||
|
||||
**URL Production:** https://persegi.nyawiji.net
|
||||
|
||||
---
|
||||
|
||||
## Teknologi
|
||||
@@ -14,7 +16,7 @@ Sistem web production-ready untuk **Organisasi Pemuda Desa Persegi**, berlokasi
|
||||
| Database | MySQL / MariaDB |
|
||||
| Frontend Publik | Blade + Tailwind CSS |
|
||||
| Realtime | Livewire |
|
||||
| Queue | Supervisor |
|
||||
| Queue | Supervisor (database driver) |
|
||||
|
||||
---
|
||||
|
||||
@@ -24,33 +26,37 @@ Sistem web production-ready untuk **Organisasi Pemuda Desa Persegi**, berlokasi
|
||||
app/
|
||||
├── Filament/
|
||||
│ ├── Resources/
|
||||
│ │ ├── Activities/ # Manajemen kegiatan
|
||||
│ │ ├── Approvals/ # Multi-approval
|
||||
│ │ ├── Audits/ # Audit internal
|
||||
│ │ ├── CashCategories/ # Kategori kas
|
||||
│ │ ├── CashRecords/ # Transaksi kas
|
||||
│ │ ├── ContactMessages/# Pesan dari publik
|
||||
│ │ ├── Divisions/ # Divisi organisasi
|
||||
│ │ ├── Posts/ # Konten publik
|
||||
│ │ ├── Users/ # Manajemen anggota
|
||||
│ │ └── Votes/ # Sistem voting
|
||||
│ │ ├── Activities/ # Manajemen kegiatan
|
||||
│ │ ├── Approvals/ # Multi-approval
|
||||
│ │ ├── Audits/ # Audit internal
|
||||
│ │ ├── CashCategories/ # Kategori kas
|
||||
│ │ ├── CashRecords/ # Transaksi kas
|
||||
│ │ ├── ContactMessages/ # Pesan dari publik
|
||||
│ │ ├── Divisions/ # Divisi organisasi
|
||||
│ │ ├── MemberDues/ # Iuran anggota
|
||||
│ │ ├── MemberPoints/ # Poin anggota
|
||||
│ │ ├── Posts/ # Konten publik
|
||||
│ │ ├── Users/ # Manajemen anggota
|
||||
│ │ └── Votes/ # Sistem voting
|
||||
│ └── Widgets/
|
||||
│ ├── StatsOverview.php # Widget dashboard utama
|
||||
│ ├── CashStatsWidget.php # Widget statistik kas
|
||||
│ └── ActivityLogWidget.php # Widget log aktivitas
|
||||
├── Models/
|
||||
│ ├── User.php, Division.php, Activity.php
|
||||
│ ├── User.php, Division.php
|
||||
│ ├── Activity.php, MemberStatusLog.php, ActivityLog.php
|
||||
│ ├── MemberDue.php, MemberPoint.php
|
||||
│ ├── CashRecord.php, CashCategory.php
|
||||
│ ├── Vote.php, VoteItem.php
|
||||
│ ├── Approval.php, ApprovalItem.php
|
||||
│ ├── Audit.php, AuditResponse.php
|
||||
│ ├── MemberStatusLog.php, ActivityLog.php
|
||||
│ ├── Post.php, ContactMessage.php
|
||||
│ └── Post.php, ContactMessage.php
|
||||
└── Observers/
|
||||
├── CashRecordObserver.php # Log + threshold approval/voting
|
||||
├── ActivityObserver.php # Log approval & eksekusi kegiatan
|
||||
├── UserObserver.php # Log perubahan status anggota
|
||||
└── VoteObserver.php # Notifikasi voting baru ke semua user
|
||||
├── ActivityObserver.php # Log approval & eksekusi kegiatan
|
||||
├── CashRecordObserver.php # Log + threshold approval/voting otomatis
|
||||
├── VoteObserver.php # Notifikasi voting baru ke semua user
|
||||
└── PostObserver.php # Poin +5 saat artikel dipublish
|
||||
```
|
||||
|
||||
---
|
||||
@@ -59,12 +65,12 @@ app/
|
||||
|
||||
| Role | Deskripsi |
|
||||
|---|---|
|
||||
| `super_admin` | Full akses, override semua, semua aksi di-log |
|
||||
| `super_admin` | Full akses via gate, semua aksi di-log |
|
||||
| `ketua` | Approval kegiatan, verifikasi kas, lihat semua data |
|
||||
| `bendahara` | Input & kelola transaksi kas |
|
||||
| `bendahara` | Input & kelola transaksi kas dan iuran |
|
||||
| `pengurus` | Submit kegiatan ke pending |
|
||||
| `anggota` | Akses terbatas, bisa voting |
|
||||
| `auditor` | Read-only + bisa buat temuan audit |
|
||||
| `anggota` | Akses terbatas, bisa voting dan buat artikel |
|
||||
| `auditor` | Read-only semua + bisa buat temuan audit |
|
||||
|
||||
---
|
||||
|
||||
@@ -82,7 +88,11 @@ Threshold otomatis saat transaksi dibuat:
|
||||
|
||||
- Transaksi yang belum diverifikasi tidak masuk ke total kas
|
||||
- Setelah `verified_at` terisi, data terkunci (tidak bisa diubah/dihapus)
|
||||
- Widget statistik kas tampil di halaman transaksi: total saldo, pemasukan/pengeluaran bulan ini, saldo bulan lalu
|
||||
|
||||
### Iuran Anggota
|
||||
|
||||
- Satu iuran per anggota per periode (format `YYYY-MM`)
|
||||
- Unique constraint `(user_id, period)` mencegah duplikasi
|
||||
|
||||
### Kegiatan
|
||||
|
||||
@@ -91,6 +101,8 @@ draft → pending → approved → (executed_at diisi)
|
||||
→ rejected
|
||||
```
|
||||
|
||||
- Draft hanya terlihat oleh kreator dan `super_admin`
|
||||
|
||||
### Voting
|
||||
|
||||
- Semua user bisa melihat dan memberi suara
|
||||
@@ -98,6 +110,15 @@ draft → pending → approved → (executed_at diisi)
|
||||
- Notifikasi ke semua user saat voting baru dibuat
|
||||
- Mayoritas >50% untuk lolos
|
||||
|
||||
### Sistem Poin
|
||||
|
||||
| Event | Poin |
|
||||
|---|---|
|
||||
| Hadir kegiatan | +10 |
|
||||
| Artikel dipublish | +5 |
|
||||
|
||||
- Duplikasi dicegah via `firstOrCreate` dengan key `(user_id, source_type, source_id)`
|
||||
|
||||
### Notifikasi
|
||||
|
||||
- Database notifications via Filament
|
||||
@@ -106,7 +127,7 @@ draft → pending → approved → (executed_at diisi)
|
||||
|
||||
### Konten Publik
|
||||
|
||||
- Website publik berbasis Blade (font: Roboto + Playfair Display)
|
||||
- Website publik berbasis Blade
|
||||
- Artikel/berita, halaman kegiatan, form kontak
|
||||
- Link ke website publik tersedia di sidebar admin
|
||||
|
||||
@@ -129,7 +150,8 @@ composer install
|
||||
cp .env.example .env
|
||||
php artisan key:generate
|
||||
php artisan migrate --seed
|
||||
php artisan shield:generate --panel=admin
|
||||
php artisan shield:generate --panel=admin --all -n
|
||||
php artisan db:seed --class=PermissionSeeder --force
|
||||
php artisan shield:super-admin --user=1
|
||||
```
|
||||
|
||||
@@ -156,15 +178,12 @@ sudo supervisorctl start persegi-worker
|
||||
### Production
|
||||
|
||||
```bash
|
||||
php artisan migrate
|
||||
php artisan shield:generate --panel=admin --all -n
|
||||
php artisan db:seed --class=PermissionSeeder --force
|
||||
php artisan permission:cache-reset
|
||||
php artisan filament:optimize-clear
|
||||
php artisan filament:optimize
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
php artisan permission:cache-reset
|
||||
php artisan filament:optimize
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Kontribusi
|
||||
|
||||
Lihat [CONTRIBUTING.md](CONTRIBUTING.md) untuk panduan berkontribusi.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Filament\Resources\CashCategories\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
@@ -11,6 +12,9 @@ class CashCategoryForm
|
||||
{
|
||||
return $schema->components([
|
||||
TextInput::make('name')->label('Nama')->required(),
|
||||
Select::make('type')->label('Tipe')
|
||||
->options(['pemasukan' => 'Pemasukan', 'pengeluaran' => 'Pengeluaran'])
|
||||
->required(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ class CashCategoriesTable
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')->label('Nama')->searchable(),
|
||||
TextColumn::make('type')->label('Tipe')
|
||||
->badge()
|
||||
->color(fn ($state) => $state === 'pemasukan' ? 'success' : 'danger')
|
||||
->formatStateUsing(fn ($state) => ucfirst($state)),
|
||||
TextColumn::make('records_count')->counts('records')->label('Transaksi'),
|
||||
])
|
||||
->recordActions([EditAction::make()])
|
||||
|
||||
@@ -16,7 +16,11 @@ class CashRecordForm
|
||||
{
|
||||
return $schema->components([
|
||||
Select::make('category_id')->label('Kategori')
|
||||
->options(CashCategory::pluck('name', 'id'))
|
||||
->options(
|
||||
CashCategory::all()->mapWithKeys(fn ($c) => [
|
||||
$c->id => $c->name . ' (' . ucfirst($c->type) . ')'
|
||||
])
|
||||
)
|
||||
->required(),
|
||||
TextInput::make('amount')->label('Jumlah (Rp)')->numeric()->required()
|
||||
->helperText('< Rp500.000: langsung | Rp500.000–2.000.000: perlu approval ketua | > Rp2.000.000: perlu voting')
|
||||
|
||||
@@ -58,6 +58,11 @@ class CashRecordsTable
|
||||
->filters([
|
||||
SelectFilter::make('category_id')->label('Kategori')
|
||||
->options(CashCategory::pluck('name', 'id')),
|
||||
SelectFilter::make('type')->label('Tipe')
|
||||
->options(['pemasukan' => 'Pemasukan', 'pengeluaran' => 'Pengeluaran'])
|
||||
->query(fn ($query, $state) => $state['value']
|
||||
? $query->whereHas('category', fn ($q) => $q->where('type', $state['value']))
|
||||
: $query),
|
||||
])
|
||||
->recordActions([
|
||||
// Ketua: approve transaksi 500rb–2jt
|
||||
|
||||
@@ -23,7 +23,7 @@ class PostResource extends Resource
|
||||
// Label dinamis sesuai role
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return auth()->user()?->can('ViewAny:Post') && auth()->user()?->can('Update:Post')
|
||||
return auth()->user()?->can('Publish:Post')
|
||||
? 'Artikel'
|
||||
: 'Artikel Saya';
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class PostResource extends Resource
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
if (auth()->user()?->can('Update:Post')) {
|
||||
if (auth()->user()?->can('Publish:Post')) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ class PostForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
$isAdmin = auth()->user()?->can('Update:Post');
|
||||
$canReview = auth()->user()?->can('Publish:Post');
|
||||
|
||||
return $schema->components([
|
||||
TextInput::make('title')->label('Judul')->required()
|
||||
@@ -28,7 +28,7 @@ class PostForm
|
||||
])
|
||||
->default('umum')->required(),
|
||||
DateTimePicker::make('published_at')->label('Tanggal Publikasi')
|
||||
->visible($isAdmin)
|
||||
->visible($canReview)
|
||||
->helperText('Kosongkan untuk menyimpan sebagai draft'),
|
||||
RichEditor::make('content')->label('Konten')->required()->columnSpanFull(),
|
||||
]);
|
||||
|
||||
@@ -7,6 +7,7 @@ use Filament\Actions\Action;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
@@ -16,7 +17,8 @@ class PostsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
$isAdmin = auth()->user()?->can('Update:Post');
|
||||
$canReview = auth()->user()?->can('Publish:Post');
|
||||
$canPublish = auth()->user()?->can('Publish:Post');
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
@@ -30,68 +32,74 @@ class PostsTable
|
||||
TextColumn::make('status')->badge()
|
||||
->color(fn ($state) => match ($state) {
|
||||
'published' => 'success',
|
||||
'approved' => 'info',
|
||||
'pending' => 'warning',
|
||||
'rejected' => 'danger',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn ($state) => match ($state) {
|
||||
'draft' => 'Draft',
|
||||
'pending' => 'Menunggu',
|
||||
'approved' => 'Disetujui',
|
||||
'published' => 'Diterbitkan',
|
||||
'rejected' => 'Ditolak',
|
||||
default => $state,
|
||||
}),
|
||||
TextColumn::make('author.name')->label('Penulis')->visible($isAdmin),
|
||||
TextColumn::make('rejection_reason')->label('Alasan Penolakan')
|
||||
->limit(40)->default('-')
|
||||
->visible(fn ($record) => $record?->status === 'rejected'),
|
||||
TextColumn::make('author.name')->label('Penulis')->visible($canReview),
|
||||
TextColumn::make('published_at')->label('Dipublikasi')
|
||||
->dateTime('d M Y')->default('-')->sortable(),
|
||||
->dateTime('d M Y')->placeholder('-')->sortable(),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('status')->options([
|
||||
'draft' => 'Draft',
|
||||
'pending' => 'Menunggu',
|
||||
'approved' => 'Disetujui',
|
||||
'published' => 'Diterbitkan',
|
||||
'rejected' => 'Ditolak',
|
||||
]),
|
||||
])
|
||||
->recordActions([
|
||||
// Untuk anggota/pengurus/bendahara: ajukan artikel
|
||||
// Member: ajukan artikel
|
||||
Action::make('submit')
|
||||
->label('Ajukan')
|
||||
->icon('heroicon-o-paper-airplane')
|
||||
->color('info')
|
||||
->requiresConfirmation()
|
||||
->visible(fn ($record) => ! $isAdmin && in_array($record->status, ['draft', 'rejected']))
|
||||
->visible(fn ($record) => ! $canReview && in_array($record->status, ['draft', 'rejected']))
|
||||
->action(function ($record): void {
|
||||
$record->update(['status' => 'pending', 'rejection_reason' => null]);
|
||||
NotificationService::toRole('ketua', 'Artikel Menunggu Persetujuan',
|
||||
NotificationService::toRole('editor', 'Artikel Menunggu Persetujuan',
|
||||
"\"{$record->title}\" oleh {$record->author->name} menunggu persetujuan.", 'warning',
|
||||
route('filament.admin.resources.posts.edit', $record));
|
||||
}),
|
||||
|
||||
// Untuk admin: approve
|
||||
Action::make('publish')
|
||||
->label('Terbitkan')
|
||||
// Editor: approve
|
||||
Action::make('approve')
|
||||
->label('Setujui')
|
||||
->icon('heroicon-o-check-circle')
|
||||
->color('success')
|
||||
->requiresConfirmation()
|
||||
->visible(fn ($record) => $isAdmin && $record->status === 'pending')
|
||||
->visible(fn ($record) => $canReview && $record->status === 'pending')
|
||||
->action(fn ($record) => tap($record->update([
|
||||
'status' => 'published',
|
||||
'published_at' => now(),
|
||||
'reviewed_by' => auth()->id(),
|
||||
'status' => 'approved',
|
||||
'approved_by' => auth()->id(),
|
||||
'approved_at' => now(),
|
||||
'reviewed_by' => auth()->id(),
|
||||
]), fn () => NotificationService::send(
|
||||
$record->author, 'Artikel Diterbitkan',
|
||||
"Artikel \"{$record->title}\" Anda telah diterbitkan.", 'success',
|
||||
$record->author, 'Artikel Disetujui',
|
||||
"Artikel \"{$record->title}\" Anda telah disetujui dan akan segera diterbitkan.", 'success',
|
||||
route('filament.admin.resources.posts.edit', $record)
|
||||
))),
|
||||
|
||||
// Untuk admin: tolak
|
||||
// Editor: tolak
|
||||
Action::make('reject')
|
||||
->label('Tolak')
|
||||
->icon('heroicon-o-x-circle')
|
||||
->color('danger')
|
||||
->visible(fn ($record) => $isAdmin && $record->status === 'pending')
|
||||
->form([
|
||||
Textarea::make('rejection_reason')->label('Alasan Penolakan')->required(),
|
||||
])
|
||||
->visible(fn ($record) => $canReview && $record->status === 'pending')
|
||||
->form([Textarea::make('rejection_reason')->label('Alasan Penolakan')->required()])
|
||||
->action(fn ($record, array $data) => tap($record->update([
|
||||
'status' => 'rejected',
|
||||
'status' => 'draft',
|
||||
'reviewed_by' => auth()->id(),
|
||||
'rejection_reason' => $data['rejection_reason'],
|
||||
]), fn () => NotificationService::send(
|
||||
@@ -100,8 +108,35 @@ class PostsTable
|
||||
route('filament.admin.resources.posts.edit', $record)
|
||||
))),
|
||||
|
||||
// Editor: publish (approved → published)
|
||||
Action::make('publish')
|
||||
->label('Terbitkan')
|
||||
->icon('heroicon-o-globe-alt')
|
||||
->color('success')
|
||||
->visible(fn ($record) => $canPublish && $record->status === 'approved')
|
||||
->form([
|
||||
DateTimePicker::make('published_at')->label('Tanggal Publikasi')
|
||||
->default(now())->required(),
|
||||
])
|
||||
->action(fn ($record, array $data) => $record->update([
|
||||
'status' => 'published',
|
||||
'published_at' => $data['published_at'],
|
||||
])),
|
||||
|
||||
// Editor: unpublish
|
||||
Action::make('unpublish')
|
||||
->label('Batalkan Publikasi')
|
||||
->icon('heroicon-o-eye-slash')
|
||||
->color('warning')
|
||||
->requiresConfirmation()
|
||||
->visible(fn ($record) => $canPublish && $record->status === 'published')
|
||||
->action(fn ($record) => $record->update([
|
||||
'status' => 'approved',
|
||||
'published_at' => null,
|
||||
])),
|
||||
|
||||
EditAction::make()
|
||||
->visible(fn ($record) => $isAdmin || in_array($record->status, ['draft', 'rejected'])),
|
||||
->visible(fn ($record) => $canReview || in_array($record->status, ['draft', 'rejected'])),
|
||||
])
|
||||
->toolbarActions([BulkActionGroup::make([DeleteBulkAction::make()])]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Users\RelationManagers;
|
||||
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class AttendanceRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'activities';
|
||||
protected static ?string $title = 'Rekap Kehadiran';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('title')->label('Kegiatan'),
|
||||
TextColumn::make('start_date')->label('Tanggal')->date('d M Y')->sortable(),
|
||||
TextColumn::make('pivot.status')->label('Status')
|
||||
->badge()
|
||||
->color(fn ($state) => match ($state) {
|
||||
'hadir' => 'success',
|
||||
'izin' => 'warning',
|
||||
'alpha' => 'danger',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn ($state) => ucfirst($state ?? '-')),
|
||||
TextColumn::make('pivot.notes')->label('Catatan')->placeholder('-'),
|
||||
])
|
||||
->defaultSort('activities.start_date', 'desc')
|
||||
->paginated([10, 25]);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Filament\Resources\Users;
|
||||
use App\Filament\Resources\Users\Pages\CreateUser;
|
||||
use App\Filament\Resources\Users\Pages\EditUser;
|
||||
use App\Filament\Resources\Users\Pages\ListUsers;
|
||||
use App\Filament\Resources\Users\RelationManagers\AttendanceRelationManager;
|
||||
use App\Filament\Resources\Users\Schemas\UserForm;
|
||||
use App\Filament\Resources\Users\Tables\UsersTable;
|
||||
use App\Models\User;
|
||||
@@ -30,6 +31,13 @@ class UserResource extends Resource
|
||||
return UsersTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
AttendanceRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -21,7 +21,7 @@ class CashStatsWidget extends StatsOverviewWidget
|
||||
$saldo = fn ($query) => $query
|
||||
->whereNotNull('verified_at')
|
||||
->join('cash_categories', 'cash_records.category_id', '=', 'cash_categories.id')
|
||||
->selectRaw("SUM(CASE WHEN cash_categories.name = 'pemasukan' THEN amount ELSE -amount END) as saldo")
|
||||
->selectRaw("SUM(CASE WHEN cash_categories.type = 'pemasukan' THEN amount ELSE -amount END) as saldo")
|
||||
->value('saldo') ?? 0;
|
||||
|
||||
$bulanIni = now()->startOfMonth();
|
||||
@@ -30,12 +30,12 @@ class CashStatsWidget extends StatsOverviewWidget
|
||||
$totalSaldo = $saldo(CashRecord::query());
|
||||
$pemasukanBulanIni = CashRecord::whereNotNull('verified_at')
|
||||
->join('cash_categories', 'cash_records.category_id', '=', 'cash_categories.id')
|
||||
->where('cash_categories.name', 'pemasukan')
|
||||
->where('cash_categories.type', 'pemasukan')
|
||||
->whereMonth('date', $bulanIni->month)->whereYear('date', $bulanIni->year)
|
||||
->sum('amount');
|
||||
$pengeluaranBulanIni = CashRecord::whereNotNull('verified_at')
|
||||
->join('cash_categories', 'cash_records.category_id', '=', 'cash_categories.id')
|
||||
->where('cash_categories.name', 'pengeluaran')
|
||||
->where('cash_categories.type', 'pengeluaran')
|
||||
->whereMonth('date', $bulanIni->month)->whereYear('date', $bulanIni->year)
|
||||
->sum('amount');
|
||||
$saldoBulanLalu = $saldo(CashRecord::query()->where('date', '<', $bulanIni));
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\MemberPoint;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class LeaderboardWidget extends TableWidget
|
||||
{
|
||||
protected static ?string $heading = 'Leaderboard Poin Anggota';
|
||||
protected int | string | array $columnSpan = 'full';
|
||||
protected static ?int $sort = 3;
|
||||
|
||||
public function getTableRecordKey(\Illuminate\Database\Eloquent\Model|array $record): string
|
||||
{
|
||||
return (string) (is_array($record) ? $record['user_id'] : $record->user_id);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query(
|
||||
MemberPoint::query()
|
||||
->selectRaw('user_id, SUM(points) as total_points')
|
||||
->groupBy('user_id')
|
||||
->orderByDesc('total_points')
|
||||
->limit(10)
|
||||
->with('user')
|
||||
)
|
||||
->columns([
|
||||
TextColumn::make('user.name')->label('Anggota'),
|
||||
TextColumn::make('total_points')->label('Total Poin')
|
||||
->badge()->color('success'),
|
||||
])
|
||||
->paginated(false);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ class StatsOverview extends StatsOverviewWidget
|
||||
{
|
||||
$totalKas = CashRecord::whereNotNull('verified_at')
|
||||
->join('cash_categories', 'cash_records.category_id', '=', 'cash_categories.id')
|
||||
->selectRaw("SUM(CASE WHEN cash_categories.name = 'pemasukan' THEN amount ELSE -amount END) as saldo")
|
||||
->selectRaw("SUM(CASE WHEN cash_categories.type = 'pemasukan' THEN amount ELSE -amount END) as saldo")
|
||||
->value('saldo') ?? 0;
|
||||
|
||||
return [
|
||||
|
||||
@@ -51,6 +51,13 @@ class PublicController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function blogDetail(Post $post)
|
||||
{
|
||||
abort_if($post->status !== 'published' || ! $post->published_at, 404);
|
||||
|
||||
return view('public.blog-detail', compact('post'));
|
||||
}
|
||||
|
||||
public function kontak()
|
||||
{
|
||||
return view('public.kontak');
|
||||
|
||||
@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class CashCategory extends Model
|
||||
{
|
||||
protected $fillable = ['name'];
|
||||
protected $fillable = ['name', 'type'];
|
||||
|
||||
public function records(): HasMany
|
||||
{
|
||||
|
||||
+7
-2
@@ -8,9 +8,9 @@ use Illuminate\Support\Str;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
protected $fillable = ['title', 'slug', 'category', 'content', 'author_id', 'published_at', 'status', 'reviewed_by', 'rejection_reason'];
|
||||
protected $fillable = ['title', 'slug', 'category', 'content', 'author_id', 'published_at', 'status', 'reviewed_by', 'rejection_reason', 'approved_by', 'approved_at'];
|
||||
|
||||
protected $casts = ['published_at' => 'datetime'];
|
||||
protected $casts = ['published_at' => 'datetime', 'approved_at' => 'datetime'];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
@@ -30,6 +30,11 @@ class Post extends Model
|
||||
return $this->belongsTo(User::class, 'reviewed_by');
|
||||
}
|
||||
|
||||
public function approver(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'approved_by');
|
||||
}
|
||||
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('status', 'published')
|
||||
|
||||
@@ -9,14 +9,18 @@ class PostObserver
|
||||
{
|
||||
public function updated(Post $post): void
|
||||
{
|
||||
if ($post->wasChanged('status') && $post->status === 'published') {
|
||||
MemberPoint::create([
|
||||
'user_id' => $post->author_id,
|
||||
'points' => 5,
|
||||
'reason' => "Artikel dipublikasi: {$post->title}",
|
||||
'source_type' => 'post',
|
||||
'source_id' => $post->id,
|
||||
]);
|
||||
if ($post->wasChanged('status') && $post->status === 'approved') {
|
||||
MemberPoint::firstOrCreate(
|
||||
[
|
||||
'user_id' => $post->author_id,
|
||||
'source_type' => Post::class,
|
||||
'source_id' => $post->id,
|
||||
],
|
||||
[
|
||||
'points' => 5,
|
||||
'reason' => "Artikel disetujui: {$post->title}",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@ class UserObserver
|
||||
|
||||
public function created(User $user): void
|
||||
{
|
||||
// Auto-assign role anggota jika belum punya role
|
||||
if ($user->roles->isEmpty()) {
|
||||
$user->assignRole('anggota');
|
||||
}
|
||||
|
||||
ActivityLog::create([
|
||||
'user_id' => Auth::id(),
|
||||
'action' => 'created',
|
||||
|
||||
@@ -27,8 +27,11 @@ class AdminPanelProvider extends PanelProvider
|
||||
{
|
||||
return $panel
|
||||
->default()
|
||||
->brandLogo(asset('images/logo.png'))
|
||||
->brandLogoHeight('2rem')
|
||||
->id('admin')
|
||||
->path('dashboard')
|
||||
->viteTheme('resources/css/filament/admin/theme.css')
|
||||
->login()
|
||||
->colors([
|
||||
'primary' => Color::Amber,
|
||||
@@ -59,6 +62,7 @@ class AdminPanelProvider extends PanelProvider
|
||||
AccountWidget::class,
|
||||
\App\Filament\Widgets\StatsOverview::class,
|
||||
\App\Filament\Widgets\ActivityLogWidget::class,
|
||||
\App\Filament\Widgets\LeaderboardWidget::class,
|
||||
])
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
|
||||
'custom_permissions' => [
|
||||
'ViewDraft:Activity', // Lihat kegiatan berstatus draft milik user lain (hanya super_admin)
|
||||
'Publish:Post', // Publish / unpublish artikel (editor)
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
@@ -10,7 +10,7 @@ return new class extends Migration
|
||||
{
|
||||
Schema::create('cash_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name'); // pemasukan / pengeluaran
|
||||
$table->string('name'); // nama bebas, tipe dikontrol via kolom type
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('cash_categories', function (Blueprint $table) {
|
||||
$table->enum('type', ['pemasukan', 'pengeluaran'])->after('name')->default('pemasukan');
|
||||
});
|
||||
|
||||
// Migrate existing data
|
||||
DB::table('cash_categories')->where('name', 'pengeluaran')->update(['type' => 'pengeluaran']);
|
||||
DB::table('cash_categories')->where('name', '!=', 'pengeluaran')->update(['type' => 'pemasukan']);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('cash_categories', function (Blueprint $table) {
|
||||
$table->dropColumn('type');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('posts', function (Blueprint $table) {
|
||||
// Ubah enum status tambah 'approved'
|
||||
$table->enum('status', ['draft', 'pending', 'approved', 'published', 'rejected'])
|
||||
->default('draft')->change();
|
||||
|
||||
$table->foreignId('approved_by')->nullable()->constrained('users')->after('reviewed_by');
|
||||
$table->timestamp('approved_at')->nullable()->after('approved_by');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('posts', function (Blueprint $table) {
|
||||
$table->dropForeign(['approved_by']);
|
||||
$table->dropColumn(['approved_by', 'approved_at']);
|
||||
$table->enum('status', ['draft', 'pending', 'published', 'rejected'])
|
||||
->default('draft')->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -47,11 +47,16 @@ class ActivitySeeder extends Seeder
|
||||
foreach ($activities as $data) {
|
||||
$activity = Activity::create(array_merge($data, ['created_by' => $pengurus?->id]));
|
||||
|
||||
// Attach peserta dengan status kehadiran
|
||||
$syncData = $anggota->mapWithKeys(fn ($user) => [
|
||||
$user->id => ['status' => 'hadir', 'notes' => null]
|
||||
])->toArray();
|
||||
$activity->participants()->sync($syncData);
|
||||
// Hanya kegiatan yang sudah executed yang punya data kehadiran
|
||||
if (! empty($data['executed_at'])) {
|
||||
$syncData = $anggota->mapWithKeys(fn ($user, $i) => [
|
||||
$user->id => [
|
||||
'status' => $i % 4 === 0 ? 'izin' : 'hadir',
|
||||
'notes' => null,
|
||||
]
|
||||
])->toArray();
|
||||
$activity->participants()->sync($syncData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Approval;
|
||||
use App\Models\CashCategory;
|
||||
use App\Models\CashRecord;
|
||||
use App\Models\User;
|
||||
@@ -11,8 +12,8 @@ class CashSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$pemasukan = CashCategory::firstOrCreate(['name' => 'pemasukan']);
|
||||
$pengeluaran = CashCategory::firstOrCreate(['name' => 'pengeluaran']);
|
||||
$pemasukan = CashCategory::firstOrCreate(['name' => 'Iuran & Donasi'], ['type' => 'pemasukan']);
|
||||
$pengeluaran = CashCategory::firstOrCreate(['name' => 'Operasional'], ['type' => 'pengeluaran']);
|
||||
|
||||
$bendahara = User::role('bendahara')->first();
|
||||
$ketua = User::role('ketua')->first();
|
||||
@@ -26,11 +27,30 @@ class CashSeeder extends Seeder
|
||||
];
|
||||
|
||||
foreach ($records as $data) {
|
||||
CashRecord::create(array_merge($data, [
|
||||
'created_by' => $bendahara?->id,
|
||||
'verified_by' => $ketua?->id,
|
||||
'verified_at' => now()->subDays(1),
|
||||
]));
|
||||
$amount = $data['amount'];
|
||||
|
||||
// 500rb–2jt: butuh approval ketua, belum verified
|
||||
if ($amount >= 500_000 && $amount <= 2_000_000) {
|
||||
$record = CashRecord::create(array_merge($data, [
|
||||
'created_by' => $bendahara?->id,
|
||||
'verified_by' => null,
|
||||
'verified_at' => null,
|
||||
]));
|
||||
|
||||
Approval::create([
|
||||
'model_type' => CashRecord::class,
|
||||
'model_id' => $record->id,
|
||||
'required_approvals' => 1,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
} else {
|
||||
// < 500rb: langsung verified
|
||||
CashRecord::create(array_merge($data, [
|
||||
'created_by' => $bendahara?->id,
|
||||
'verified_by' => $ketua?->id,
|
||||
'verified_at' => now()->subDays(1),
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\ContactMessage;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class ContactMessageSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$messages = [
|
||||
['name' => 'Budi Santoso', 'email' => 'budi@example.com', 'phone' => '08123456789', 'subject' => 'Pendaftaran anggota baru', 'message' => 'Saya ingin bergabung dengan organisasi Persegi. Bagaimana cara mendaftarnya?'],
|
||||
['name' => 'Siti Rahayu', 'email' => null, 'phone' => '08987654321', 'subject' => 'Informasi kegiatan kerja bakti', 'message' => 'Apakah masyarakat umum boleh ikut kegiatan kerja bakti yang akan datang?'],
|
||||
['name' => 'Ahmad Fauzi', 'email' => 'ahmad@example.com', 'phone' => null, 'subject' => 'Donasi untuk kegiatan', 'message' => 'Saya ingin berdonasi untuk mendukung kegiatan. Kemana saya bisa menghubungi bendahara?'],
|
||||
['name' => 'Dewi Lestari', 'email' => 'dewi@example.com', 'phone' => '08111222333', 'subject' => 'Jadwal rapat bulanan', 'message' => 'Kapan jadwal rapat bulanan berikutnya? Saya ingin hadir sebagai tamu.'],
|
||||
['name' => 'Hendra Wijaya', 'email' => null, 'phone' => '08555666777', 'subject' => 'Laporan kerusakan fasilitas desa', 'message' => 'Ada lampu jalan yang mati di RT 03. Apakah Persegi bisa membantu melaporkan ke desa?'],
|
||||
['name' => 'Rina Kusuma', 'email' => 'rina@example.com', 'phone' => '08222333444', 'subject' => 'Kerjasama kegiatan sosial', 'message' => 'Kami dari karang taruna RT 05 ingin mengajak kerjasama untuk kegiatan sosial bulan depan.'],
|
||||
['name' => 'Joko Pramono', 'email' => null, 'phone' => '08444555666', 'subject' => 'Pertanyaan iuran anggota', 'message' => 'Berapa besaran iuran bulanan anggota Persegi? Saya tertarik untuk bergabung.'],
|
||||
['name' => 'Nurul Hidayah', 'email' => 'nurul@example.com', 'phone' => '08777888999', 'subject' => 'Saran program kerja', 'message' => 'Saya punya usulan program pelatihan digital untuk pemuda desa. Bagaimana cara menyampaikannya?'],
|
||||
];
|
||||
|
||||
foreach ($messages as $data) {
|
||||
ContactMessage::create($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ class DatabaseSeeder extends Seeder
|
||||
VoteSeeder::class,
|
||||
PostSeeder::class,
|
||||
AuditSeeder::class,
|
||||
MemberDueSeeder::class,
|
||||
ContactMessageSeeder::class,
|
||||
MemberPointSeeder::class,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\MemberDue;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class MemberDueSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$bendahara = User::role('bendahara')->first();
|
||||
|
||||
$members = User::whereDoesntHave('roles', fn ($q) => $q->whereIn('name', ['super_admin', 'bendahara']))
|
||||
->get();
|
||||
|
||||
$periods = [
|
||||
now()->subMonths(3)->format('Y-m'),
|
||||
now()->subMonths(2)->format('Y-m'),
|
||||
now()->subMonth()->format('Y-m'),
|
||||
now()->format('Y-m'),
|
||||
];
|
||||
|
||||
foreach ($members as $i => $user) {
|
||||
foreach ($periods as $j => $period) {
|
||||
MemberDue::firstOrCreate(
|
||||
['user_id' => $user->id, 'period' => $period],
|
||||
[
|
||||
'amount' => 10000,
|
||||
'status' => ($i + $j) % 3 === 0 ? 'belum' : 'lunas',
|
||||
'created_by' => $bendahara?->id,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,17 +16,17 @@ class MemberPointSeeder extends Seeder
|
||||
Activity::whereNotNull('executed_at')->each(function ($activity) {
|
||||
$activity->participants()->wherePivot('status', 'hadir')->each(function ($user) use ($activity) {
|
||||
MemberPoint::firstOrCreate(
|
||||
['user_id' => $user->id, 'source_type' => 'activity', 'source_id' => $activity->id],
|
||||
['user_id' => $user->id, 'source_type' => Activity::class, 'source_id' => $activity->id],
|
||||
['points' => 10, 'reason' => "Hadir di kegiatan: {$activity->title}"]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Poin dari artikel yang sudah published
|
||||
Post::where('status', 'published')->each(function ($post) {
|
||||
// Poin dari artikel yang sudah approved
|
||||
Post::where('status', 'approved')->orWhere('status', 'published')->each(function ($post) {
|
||||
MemberPoint::firstOrCreate(
|
||||
['user_id' => $post->author_id, 'source_type' => 'post', 'source_id' => $post->id],
|
||||
['points' => 5, 'reason' => "Artikel dipublikasi: {$post->title}"]
|
||||
['user_id' => $post->author_id, 'source_type' => Post::class, 'source_id' => $post->id],
|
||||
['points' => 5, 'reason' => "Artikel disetujui: {$post->title}"]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class PermissionSeeder extends Seeder
|
||||
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
|
||||
// Buat roles jika belum ada
|
||||
foreach (['super_admin', 'ketua', 'bendahara', 'pengurus', 'anggota', 'auditor'] as $role) {
|
||||
foreach (['super_admin', 'ketua', 'bendahara', 'pengurus', 'anggota', 'auditor', 'editor'] as $role) {
|
||||
Role::firstOrCreate(['name' => $role, 'guard_name' => 'web']);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class PermissionSeeder extends Seeder
|
||||
$pengurus = Role::findByName('pengurus');
|
||||
$anggota = Role::findByName('anggota');
|
||||
$auditor = Role::findByName('auditor');
|
||||
$editor = Role::findByName('editor');
|
||||
|
||||
$ketua->syncPermissions(Permission::where('name', 'not like', '%Role%')
|
||||
->where('name', 'not like', '%Permission%')
|
||||
@@ -63,5 +64,11 @@ class PermissionSeeder extends Seeder
|
||||
->orWhere('name', 'like', 'View:%')
|
||||
->orWhere('name', 'like', '%Audit%')
|
||||
->get());
|
||||
|
||||
$editor->syncPermissions(Permission::whereIn('name', [
|
||||
'ViewAny:Post', 'View:Post', 'Create:Post', 'Update:Post', 'Delete:Post',
|
||||
'DeleteAny:Post', 'ViewAny:Activity', 'View:Activity',
|
||||
'Publish:Post',
|
||||
])->get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,38 +5,87 @@ namespace Database\Seeders;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PostSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$author = User::role('ketua')->first() ?? User::first();
|
||||
$editor = User::role('editor')->first();
|
||||
$anggota = User::role('anggota')->first();
|
||||
$ketua = User::role('ketua')->first();
|
||||
|
||||
$posts = [
|
||||
// Published — sudah melalui full workflow
|
||||
[
|
||||
'title' => 'Selamat Datang di Website Persegi',
|
||||
'category' => 'pengumuman',
|
||||
'content' => '<p>Kami dengan bangga mempersembahkan website resmi organisasi Persegi. Melalui website ini, masyarakat dapat mengikuti perkembangan kegiatan dan informasi terbaru dari organisasi kami.</p>',
|
||||
'author_id' => $ketua?->id,
|
||||
'status' => 'published',
|
||||
'approved_by' => $editor?->id,
|
||||
'approved_at' => now()->subDays(11),
|
||||
'reviewed_by' => $editor?->id,
|
||||
'published_at' => now()->subDays(10),
|
||||
],
|
||||
[
|
||||
'title' => 'Rekrutmen Anggota Baru 2026',
|
||||
'category' => 'pengumuman',
|
||||
'content' => '<p>Persegi membuka pendaftaran anggota baru untuk periode 2026. Bagi pemuda Desa Karangdadap yang ingin bergabung, silakan hubungi pengurus melalui kontak yang tersedia.</p><p>Pendaftaran dibuka hingga akhir bulan April 2026.</p>',
|
||||
'author_id' => $ketua?->id,
|
||||
'status' => 'published',
|
||||
'approved_by' => $editor?->id,
|
||||
'approved_at' => now()->subDays(6),
|
||||
'reviewed_by' => $editor?->id,
|
||||
'published_at' => now()->subDays(5),
|
||||
],
|
||||
[
|
||||
'title' => 'Laporan Kegiatan Kerja Bakti Desa',
|
||||
'category' => 'berita',
|
||||
'content' => '<p>Kegiatan kerja bakti yang dilaksanakan pada bulan lalu berjalan dengan lancar. Sebanyak 30 anggota turut berpartisipasi dalam membersihkan lingkungan desa.</p><p>Terima kasih kepada seluruh anggota yang telah berkontribusi.</p>',
|
||||
'author_id' => $anggota?->id,
|
||||
'status' => 'published',
|
||||
'approved_by' => $editor?->id,
|
||||
'approved_at' => now()->subDays(3),
|
||||
'reviewed_by' => $editor?->id,
|
||||
'published_at' => now()->subDays(2),
|
||||
],
|
||||
// Approved — sudah disetujui editor, belum diterbitkan
|
||||
[
|
||||
'title' => 'Jadwal Rapat Bulanan April 2026',
|
||||
'category' => 'pengumuman',
|
||||
'content' => '<p>Rapat bulanan akan dilaksanakan pada akhir April 2026. Seluruh anggota diharapkan hadir.</p>',
|
||||
'author_id' => $anggota?->id,
|
||||
'status' => 'approved',
|
||||
'approved_by' => $editor?->id,
|
||||
'approved_at' => now()->subDay(),
|
||||
'reviewed_by' => $editor?->id,
|
||||
'published_at' => null,
|
||||
],
|
||||
// Pending — menunggu review editor
|
||||
[
|
||||
'title' => 'Kegiatan Sosial Ramadan 2026',
|
||||
'category' => 'berita',
|
||||
'content' => '<p>Persegi berencana mengadakan kegiatan sosial berbagi sembako selama bulan Ramadan.</p>',
|
||||
'author_id' => $anggota?->id,
|
||||
'status' => 'pending',
|
||||
'published_at' => null,
|
||||
],
|
||||
// Draft — belum diajukan
|
||||
[
|
||||
'title' => 'Profil Divisi Olahraga',
|
||||
'category' => 'umum',
|
||||
'content' => '<p>Divisi olahraga Persegi aktif mengadakan kegiatan rutin setiap minggu.</p>',
|
||||
'author_id' => $anggota?->id,
|
||||
'status' => 'draft',
|
||||
'published_at' => null,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($posts as $data) {
|
||||
Post::firstOrCreate(
|
||||
['slug' => \Illuminate\Support\Str::slug($data['title'])],
|
||||
array_merge($data, ['author_id' => $author->id, 'status' => 'published'])
|
||||
['slug' => Str::slug($data['title'])],
|
||||
$data
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,15 @@ class UserSeeder extends Seeder
|
||||
->each(fn ($user) => $user->assignRole($role));
|
||||
}
|
||||
|
||||
// 1 editor
|
||||
User::factory()->createOne([
|
||||
'name' => 'Editor Konten',
|
||||
'email' => 'editor@persegi.test',
|
||||
'password' => bcrypt('password'),
|
||||
'status' => 'aktif',
|
||||
'division_id' => fake()->randomElement($divisions),
|
||||
])->assignRole('editor');
|
||||
|
||||
// 2 user tanpa role
|
||||
User::factory(2)->create(['division_id' => fake()->randomElement($divisions)]);
|
||||
}
|
||||
|
||||
Generated
+1912
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -7,11 +7,11 @@
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"axios": ">=1.11.0 <=1.14.0",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^3.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
+15
-2
@@ -6,6 +6,19 @@
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-sans: 'Roboto', ui-sans-serif, system-ui, sans-serif;
|
||||
--font-heading: 'Playfair Display', ui-serif, serif;
|
||||
}
|
||||
|
||||
[x-cloak] { display: none; }
|
||||
|
||||
.line-clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
|
||||
.prose p { margin-bottom: 1rem; }
|
||||
|
||||
.skew-top { clip-path: polygon(0 4%, 100% 0, 100% 100%, 0 100%); margin-top: -2rem; padding-top: 5rem; }
|
||||
.skew-bottom { clip-path: polygon(0 0, 100% 0, 100% 96%, 0 100%); padding-bottom: 5rem; }
|
||||
.skew-both { clip-path: polygon(0 4%, 100% 0, 100% 96%, 0 100%); margin-top: -2rem; padding-top: 5rem; padding-bottom: 5rem; }
|
||||
|
||||
h1, h2, h3, h4, h5, h6 { font-family: 'Playfair Display', serif; }
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@import '../../../../vendor/filament/filament/resources/css/theme.css';
|
||||
|
||||
@source '../../../../app/Filament/**/*';
|
||||
@source '../../../../resources/views/filament/**/*';
|
||||
@@ -4,32 +4,10 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@yield('title', 'Persegi') — Organisasi Pemuda Desa Karangdadap</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Roboto', 'sans-serif'],
|
||||
heading: ['Playfair Display', 'serif'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
[x-cloak] { display: none; }
|
||||
.line-clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.prose p { margin-bottom: 1rem; }
|
||||
.skew-top { clip-path: polygon(0 4%, 100% 0, 100% 100%, 0 100%); margin-top: -2rem; padding-top: 5rem; }
|
||||
.skew-bottom { clip-path: polygon(0 0, 100% 0, 100% 96%, 0 100%); padding-bottom: 5rem; }
|
||||
.skew-both { clip-path: polygon(0 4%, 100% 0, 100% 96%, 0 100%); margin-top: -2rem; padding-top: 5rem; padding-bottom: 5rem; }
|
||||
h1, h2, h3, h4, h5, h6 { font-family: 'Playfair Display', serif; }
|
||||
</style>
|
||||
@vite('resources/css/app.css')
|
||||
</head>
|
||||
<body class="bg-white text-gray-900 font-sans antialiased" style="font-family: 'Roboto', sans-serif;">
|
||||
|
||||
|
||||
+4
-1
@@ -1,16 +1,19 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import laravel from 'laravel-vite-plugin';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import os from 'os';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
laravel({
|
||||
input: ['resources/css/app.css', 'resources/js/app.js'],
|
||||
input: ['resources/css/app.css', 'resources/js/app.js', 'resources/css/filament/admin/theme.css'],
|
||||
refresh: true,
|
||||
}),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
host: os.networkInterfaces().eth0?.[0].address,
|
||||
cors: true,
|
||||
watch: {
|
||||
ignored: ['**/storage/framework/views/**'],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user