chore: merge dev ke master

This commit is contained in:
2026-04-05 06:47:06 +07:00
40 changed files with 2432 additions and 129 deletions
+2 -1
View File
@@ -34,7 +34,8 @@ DB_PASSWORD=
Lalu jalankan: Lalu jalankan:
```bash ```bash
php artisan migrate --seed 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 shield:super-admin --user=1
php artisan serve php artisan serve
``` ```
+52 -33
View File
@@ -2,6 +2,8 @@
Sistem web production-ready untuk **Organisasi Pemuda Desa Persegi**, berlokasi di Desa Karangdadap, Kecamatan Kalibagor, Kabupaten Banyumas. Sistem web production-ready untuk **Organisasi Pemuda Desa Persegi**, berlokasi di Desa Karangdadap, Kecamatan Kalibagor, Kabupaten Banyumas.
**URL Production:** https://persegi.nyawiji.net
--- ---
## Teknologi ## Teknologi
@@ -14,7 +16,7 @@ Sistem web production-ready untuk **Organisasi Pemuda Desa Persegi**, berlokasi
| Database | MySQL / MariaDB | | Database | MySQL / MariaDB |
| Frontend Publik | Blade + Tailwind CSS | | Frontend Publik | Blade + Tailwind CSS |
| Realtime | Livewire | | Realtime | Livewire |
| Queue | Supervisor | | Queue | Supervisor (database driver) |
--- ---
@@ -24,33 +26,37 @@ Sistem web production-ready untuk **Organisasi Pemuda Desa Persegi**, berlokasi
app/ app/
├── Filament/ ├── Filament/
│ ├── Resources/ │ ├── Resources/
│ │ ├── Activities/ # Manajemen kegiatan │ │ ├── Activities/ # Manajemen kegiatan
│ │ ├── Approvals/ # Multi-approval │ │ ├── Approvals/ # Multi-approval
│ │ ├── Audits/ # Audit internal │ │ ├── Audits/ # Audit internal
│ │ ├── CashCategories/ # Kategori kas │ │ ├── CashCategories/ # Kategori kas
│ │ ├── CashRecords/ # Transaksi kas │ │ ├── CashRecords/ # Transaksi kas
│ │ ├── ContactMessages/# Pesan dari publik │ │ ├── ContactMessages/ # Pesan dari publik
│ │ ├── Divisions/ # Divisi organisasi │ │ ├── Divisions/ # Divisi organisasi
│ │ ├── Posts/ # Konten publik │ │ ├── MemberDues/ # Iuran anggota
│ │ ├── Users/ # Manajemen anggota │ │ ├── MemberPoints/ # Poin anggota
│ │ ── Votes/ # Sistem voting │ │ ── Posts/ # Konten publik
│ │ ├── Users/ # Manajemen anggota
│ │ └── Votes/ # Sistem voting
│ └── Widgets/ │ └── Widgets/
│ ├── StatsOverview.php # Widget dashboard utama │ ├── StatsOverview.php # Widget dashboard utama
│ ├── CashStatsWidget.php # Widget statistik kas │ ├── CashStatsWidget.php # Widget statistik kas
│ └── ActivityLogWidget.php # Widget log aktivitas │ └── ActivityLogWidget.php # Widget log aktivitas
├── Models/ ├── 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 │ ├── CashRecord.php, CashCategory.php
│ ├── Vote.php, VoteItem.php │ ├── Vote.php, VoteItem.php
│ ├── Approval.php, ApprovalItem.php │ ├── Approval.php, ApprovalItem.php
│ ├── Audit.php, AuditResponse.php │ ├── Audit.php, AuditResponse.php
── MemberStatusLog.php, ActivityLog.php ── Post.php, ContactMessage.php
│ ├── Post.php, ContactMessage.php
└── Observers/ └── Observers/
├── CashRecordObserver.php # Log + threshold approval/voting
├── ActivityObserver.php # Log approval & eksekusi kegiatan
├── UserObserver.php # Log perubahan status anggota ├── 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 | | 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 | | `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 | | `pengurus` | Submit kegiatan ke pending |
| `anggota` | Akses terbatas, bisa voting | | `anggota` | Akses terbatas, bisa voting dan buat artikel |
| `auditor` | Read-only + bisa buat temuan audit | | `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 - Transaksi yang belum diverifikasi tidak masuk ke total kas
- Setelah `verified_at` terisi, data terkunci (tidak bisa diubah/dihapus) - 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 ### Kegiatan
@@ -91,6 +101,8 @@ draft → pending → approved → (executed_at diisi)
→ rejected → rejected
``` ```
- Draft hanya terlihat oleh kreator dan `super_admin`
### Voting ### Voting
- Semua user bisa melihat dan memberi suara - 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 - Notifikasi ke semua user saat voting baru dibuat
- Mayoritas >50% untuk lolos - 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 ### Notifikasi
- Database notifications via Filament - Database notifications via Filament
@@ -106,7 +127,7 @@ draft → pending → approved → (executed_at diisi)
### Konten Publik ### Konten Publik
- Website publik berbasis Blade (font: Roboto + Playfair Display) - Website publik berbasis Blade
- Artikel/berita, halaman kegiatan, form kontak - Artikel/berita, halaman kegiatan, form kontak
- Link ke website publik tersedia di sidebar admin - Link ke website publik tersedia di sidebar admin
@@ -129,7 +150,8 @@ composer install
cp .env.example .env cp .env.example .env
php artisan key:generate php artisan key:generate
php artisan migrate --seed 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 shield:super-admin --user=1
``` ```
@@ -156,15 +178,12 @@ sudo supervisorctl start persegi-worker
### Production ### Production
```bash ```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 config:cache
php artisan route: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; namespace App\Filament\Resources\CashCategories\Schemas;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
@@ -11,6 +12,9 @@ class CashCategoryForm
{ {
return $schema->components([ return $schema->components([
TextInput::make('name')->label('Nama')->required(), 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 return $table
->columns([ ->columns([
TextColumn::make('name')->label('Nama')->searchable(), 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'), TextColumn::make('records_count')->counts('records')->label('Transaksi'),
]) ])
->recordActions([EditAction::make()]) ->recordActions([EditAction::make()])
@@ -16,7 +16,11 @@ class CashRecordForm
{ {
return $schema->components([ return $schema->components([
Select::make('category_id')->label('Kategori') 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(), ->required(),
TextInput::make('amount')->label('Jumlah (Rp)')->numeric()->required() TextInput::make('amount')->label('Jumlah (Rp)')->numeric()->required()
->helperText('< Rp500.000: langsung | Rp500.0002.000.000: perlu approval ketua | > Rp2.000.000: perlu voting') ->helperText('< Rp500.000: langsung | Rp500.0002.000.000: perlu approval ketua | > Rp2.000.000: perlu voting')
@@ -58,6 +58,11 @@ class CashRecordsTable
->filters([ ->filters([
SelectFilter::make('category_id')->label('Kategori') SelectFilter::make('category_id')->label('Kategori')
->options(CashCategory::pluck('name', 'id')), ->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([ ->recordActions([
// Ketua: approve transaksi 500rb2jt // Ketua: approve transaksi 500rb2jt
@@ -23,7 +23,7 @@ class PostResource extends Resource
// Label dinamis sesuai role // Label dinamis sesuai role
public static function getModelLabel(): string public static function getModelLabel(): string
{ {
return auth()->user()?->can('ViewAny:Post') && auth()->user()?->can('Update:Post') return auth()->user()?->can('Publish:Post')
? 'Artikel' ? 'Artikel'
: 'Artikel Saya'; : 'Artikel Saya';
} }
@@ -32,7 +32,7 @@ class PostResource extends Resource
{ {
$query = parent::getEloquentQuery(); $query = parent::getEloquentQuery();
if (auth()->user()?->can('Update:Post')) { if (auth()->user()?->can('Publish:Post')) {
return $query; return $query;
} }
@@ -13,7 +13,7 @@ class PostForm
{ {
public static function configure(Schema $schema): Schema public static function configure(Schema $schema): Schema
{ {
$isAdmin = auth()->user()?->can('Update:Post'); $canReview = auth()->user()?->can('Publish:Post');
return $schema->components([ return $schema->components([
TextInput::make('title')->label('Judul')->required() TextInput::make('title')->label('Judul')->required()
@@ -28,7 +28,7 @@ class PostForm
]) ])
->default('umum')->required(), ->default('umum')->required(),
DateTimePicker::make('published_at')->label('Tanggal Publikasi') DateTimePicker::make('published_at')->label('Tanggal Publikasi')
->visible($isAdmin) ->visible($canReview)
->helperText('Kosongkan untuk menyimpan sebagai draft'), ->helperText('Kosongkan untuk menyimpan sebagai draft'),
RichEditor::make('content')->label('Konten')->required()->columnSpanFull(), RichEditor::make('content')->label('Konten')->required()->columnSpanFull(),
]); ]);
@@ -7,6 +7,7 @@ use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup; use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction; use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
@@ -16,7 +17,8 @@ class PostsTable
{ {
public static function configure(Table $table): Table 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 return $table
->columns([ ->columns([
@@ -30,68 +32,74 @@ class PostsTable
TextColumn::make('status')->badge() TextColumn::make('status')->badge()
->color(fn ($state) => match ($state) { ->color(fn ($state) => match ($state) {
'published' => 'success', 'published' => 'success',
'approved' => 'info',
'pending' => 'warning', 'pending' => 'warning',
'rejected' => 'danger', 'rejected' => 'danger',
default => 'gray', 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('author.name')->label('Penulis')->visible($canReview),
TextColumn::make('rejection_reason')->label('Alasan Penolakan')
->limit(40)->default('-')
->visible(fn ($record) => $record?->status === 'rejected'),
TextColumn::make('published_at')->label('Dipublikasi') TextColumn::make('published_at')->label('Dipublikasi')
->dateTime('d M Y')->default('-')->sortable(), ->dateTime('d M Y')->placeholder('-')->sortable(),
]) ])
->filters([ ->filters([
SelectFilter::make('status')->options([ SelectFilter::make('status')->options([
'draft' => 'Draft', 'draft' => 'Draft',
'pending' => 'Menunggu', 'pending' => 'Menunggu',
'approved' => 'Disetujui',
'published' => 'Diterbitkan', 'published' => 'Diterbitkan',
'rejected' => 'Ditolak', 'rejected' => 'Ditolak',
]), ]),
]) ])
->recordActions([ ->recordActions([
// Untuk anggota/pengurus/bendahara: ajukan artikel // Member: ajukan artikel
Action::make('submit') Action::make('submit')
->label('Ajukan') ->label('Ajukan')
->icon('heroicon-o-paper-airplane') ->icon('heroicon-o-paper-airplane')
->color('info') ->color('info')
->requiresConfirmation() ->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 { ->action(function ($record): void {
$record->update(['status' => 'pending', 'rejection_reason' => null]); $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', "\"{$record->title}\" oleh {$record->author->name} menunggu persetujuan.", 'warning',
route('filament.admin.resources.posts.edit', $record)); route('filament.admin.resources.posts.edit', $record));
}), }),
// Untuk admin: approve // Editor: approve
Action::make('publish') Action::make('approve')
->label('Terbitkan') ->label('Setujui')
->icon('heroicon-o-check-circle') ->icon('heroicon-o-check-circle')
->color('success') ->color('success')
->requiresConfirmation() ->requiresConfirmation()
->visible(fn ($record) => $isAdmin && $record->status === 'pending') ->visible(fn ($record) => $canReview && $record->status === 'pending')
->action(fn ($record) => tap($record->update([ ->action(fn ($record) => tap($record->update([
'status' => 'published', 'status' => 'approved',
'published_at' => now(), 'approved_by' => auth()->id(),
'reviewed_by' => auth()->id(), 'approved_at' => now(),
'reviewed_by' => auth()->id(),
]), fn () => NotificationService::send( ]), fn () => NotificationService::send(
$record->author, 'Artikel Diterbitkan', $record->author, 'Artikel Disetujui',
"Artikel \"{$record->title}\" Anda telah diterbitkan.", 'success', "Artikel \"{$record->title}\" Anda telah disetujui dan akan segera diterbitkan.", 'success',
route('filament.admin.resources.posts.edit', $record) route('filament.admin.resources.posts.edit', $record)
))), ))),
// Untuk admin: tolak // Editor: tolak
Action::make('reject') Action::make('reject')
->label('Tolak') ->label('Tolak')
->icon('heroicon-o-x-circle') ->icon('heroicon-o-x-circle')
->color('danger') ->color('danger')
->visible(fn ($record) => $isAdmin && $record->status === 'pending') ->visible(fn ($record) => $canReview && $record->status === 'pending')
->form([ ->form([Textarea::make('rejection_reason')->label('Alasan Penolakan')->required()])
Textarea::make('rejection_reason')->label('Alasan Penolakan')->required(),
])
->action(fn ($record, array $data) => tap($record->update([ ->action(fn ($record, array $data) => tap($record->update([
'status' => 'rejected', 'status' => 'draft',
'reviewed_by' => auth()->id(), 'reviewed_by' => auth()->id(),
'rejection_reason' => $data['rejection_reason'], 'rejection_reason' => $data['rejection_reason'],
]), fn () => NotificationService::send( ]), fn () => NotificationService::send(
@@ -100,8 +108,35 @@ class PostsTable
route('filament.admin.resources.posts.edit', $record) 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() 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()])]); ->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\CreateUser;
use App\Filament\Resources\Users\Pages\EditUser; use App\Filament\Resources\Users\Pages\EditUser;
use App\Filament\Resources\Users\Pages\ListUsers; 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\Schemas\UserForm;
use App\Filament\Resources\Users\Tables\UsersTable; use App\Filament\Resources\Users\Tables\UsersTable;
use App\Models\User; use App\Models\User;
@@ -30,6 +31,13 @@ class UserResource extends Resource
return UsersTable::configure($table); return UsersTable::configure($table);
} }
public static function getRelations(): array
{
return [
AttendanceRelationManager::class,
];
}
public static function getPages(): array public static function getPages(): array
{ {
return [ return [
+3 -3
View File
@@ -21,7 +21,7 @@ class CashStatsWidget extends StatsOverviewWidget
$saldo = fn ($query) => $query $saldo = fn ($query) => $query
->whereNotNull('verified_at') ->whereNotNull('verified_at')
->join('cash_categories', 'cash_records.category_id', '=', 'cash_categories.id') ->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; ->value('saldo') ?? 0;
$bulanIni = now()->startOfMonth(); $bulanIni = now()->startOfMonth();
@@ -30,12 +30,12 @@ class CashStatsWidget extends StatsOverviewWidget
$totalSaldo = $saldo(CashRecord::query()); $totalSaldo = $saldo(CashRecord::query());
$pemasukanBulanIni = CashRecord::whereNotNull('verified_at') $pemasukanBulanIni = CashRecord::whereNotNull('verified_at')
->join('cash_categories', 'cash_records.category_id', '=', 'cash_categories.id') ->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) ->whereMonth('date', $bulanIni->month)->whereYear('date', $bulanIni->year)
->sum('amount'); ->sum('amount');
$pengeluaranBulanIni = CashRecord::whereNotNull('verified_at') $pengeluaranBulanIni = CashRecord::whereNotNull('verified_at')
->join('cash_categories', 'cash_records.category_id', '=', 'cash_categories.id') ->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) ->whereMonth('date', $bulanIni->month)->whereYear('date', $bulanIni->year)
->sum('amount'); ->sum('amount');
$saldoBulanLalu = $saldo(CashRecord::query()->where('date', '<', $bulanIni)); $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);
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ class StatsOverview extends StatsOverviewWidget
{ {
$totalKas = CashRecord::whereNotNull('verified_at') $totalKas = CashRecord::whereNotNull('verified_at')
->join('cash_categories', 'cash_records.category_id', '=', 'cash_categories.id') ->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; ->value('saldo') ?? 0;
return [ 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() public function kontak()
{ {
return view('public.kontak'); return view('public.kontak');
+1 -1
View File
@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class CashCategory extends Model class CashCategory extends Model
{ {
protected $fillable = ['name']; protected $fillable = ['name', 'type'];
public function records(): HasMany public function records(): HasMany
{ {
+7 -2
View File
@@ -8,9 +8,9 @@ use Illuminate\Support\Str;
class Post extends Model 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 protected static function booted(): void
{ {
@@ -30,6 +30,11 @@ class Post extends Model
return $this->belongsTo(User::class, 'reviewed_by'); return $this->belongsTo(User::class, 'reviewed_by');
} }
public function approver(): BelongsTo
{
return $this->belongsTo(User::class, 'approved_by');
}
public function scopePublished($query) public function scopePublished($query)
{ {
return $query->where('status', 'published') return $query->where('status', 'published')
+12 -8
View File
@@ -9,14 +9,18 @@ class PostObserver
{ {
public function updated(Post $post): void public function updated(Post $post): void
{ {
if ($post->wasChanged('status') && $post->status === 'published') { if ($post->wasChanged('status') && $post->status === 'approved') {
MemberPoint::create([ MemberPoint::firstOrCreate(
'user_id' => $post->author_id, [
'points' => 5, 'user_id' => $post->author_id,
'reason' => "Artikel dipublikasi: {$post->title}", 'source_type' => Post::class,
'source_type' => 'post', 'source_id' => $post->id,
'source_id' => $post->id, ],
]); [
'points' => 5,
'reason' => "Artikel disetujui: {$post->title}",
]
);
} }
} }
} }
+5
View File
@@ -41,6 +41,11 @@ class UserObserver
public function created(User $user): void public function created(User $user): void
{ {
// Auto-assign role anggota jika belum punya role
if ($user->roles->isEmpty()) {
$user->assignRole('anggota');
}
ActivityLog::create([ ActivityLog::create([
'user_id' => Auth::id(), 'user_id' => Auth::id(),
'action' => 'created', 'action' => 'created',
@@ -27,8 +27,11 @@ class AdminPanelProvider extends PanelProvider
{ {
return $panel return $panel
->default() ->default()
->brandLogo(asset('images/logo.png'))
->brandLogoHeight('2rem')
->id('admin') ->id('admin')
->path('dashboard') ->path('dashboard')
->viteTheme('resources/css/filament/admin/theme.css')
->login() ->login()
->colors([ ->colors([
'primary' => Color::Amber, 'primary' => Color::Amber,
@@ -59,6 +62,7 @@ class AdminPanelProvider extends PanelProvider
AccountWidget::class, AccountWidget::class,
\App\Filament\Widgets\StatsOverview::class, \App\Filament\Widgets\StatsOverview::class,
\App\Filament\Widgets\ActivityLogWidget::class, \App\Filament\Widgets\ActivityLogWidget::class,
\App\Filament\Widgets\LeaderboardWidget::class,
]) ])
->middleware([ ->middleware([
EncryptCookies::class, EncryptCookies::class,
+1
View File
@@ -234,6 +234,7 @@ return [
'custom_permissions' => [ 'custom_permissions' => [
'ViewDraft:Activity', // Lihat kegiatan berstatus draft milik user lain (hanya super_admin) '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) { Schema::create('cash_categories', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('name'); // pemasukan / pengeluaran $table->string('name'); // nama bebas, tipe dikontrol via kolom type
$table->timestamps(); $table->timestamps();
}); });
} }
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('cash_categories', function (Blueprint $table) {
$table->enum('type', ['pemasukan', 'pengeluaran'])->after('name')->default('pemasukan');
});
// Migrate existing data
DB::table('cash_categories')->where('name', 'pengeluaran')->update(['type' => 'pengeluaran']);
DB::table('cash_categories')->where('name', '!=', 'pengeluaran')->update(['type' => 'pemasukan']);
}
public function down(): void
{
Schema::table('cash_categories', function (Blueprint $table) {
$table->dropColumn('type');
});
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
// Ubah enum status tambah 'approved'
$table->enum('status', ['draft', 'pending', 'approved', 'published', 'rejected'])
->default('draft')->change();
$table->foreignId('approved_by')->nullable()->constrained('users')->after('reviewed_by');
$table->timestamp('approved_at')->nullable()->after('approved_by');
});
}
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropForeign(['approved_by']);
$table->dropColumn(['approved_by', 'approved_at']);
$table->enum('status', ['draft', 'pending', 'published', 'rejected'])
->default('draft')->change();
});
}
};
+10 -5
View File
@@ -47,11 +47,16 @@ class ActivitySeeder extends Seeder
foreach ($activities as $data) { foreach ($activities as $data) {
$activity = Activity::create(array_merge($data, ['created_by' => $pengurus?->id])); $activity = Activity::create(array_merge($data, ['created_by' => $pengurus?->id]));
// Attach peserta dengan status kehadiran // Hanya kegiatan yang sudah executed yang punya data kehadiran
$syncData = $anggota->mapWithKeys(fn ($user) => [ if (! empty($data['executed_at'])) {
$user->id => ['status' => 'hadir', 'notes' => null] $syncData = $anggota->mapWithKeys(fn ($user, $i) => [
])->toArray(); $user->id => [
$activity->participants()->sync($syncData); 'status' => $i % 4 === 0 ? 'izin' : 'hadir',
'notes' => null,
]
])->toArray();
$activity->participants()->sync($syncData);
}
} }
} }
} }
+27 -7
View File
@@ -2,6 +2,7 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Models\Approval;
use App\Models\CashCategory; use App\Models\CashCategory;
use App\Models\CashRecord; use App\Models\CashRecord;
use App\Models\User; use App\Models\User;
@@ -11,8 +12,8 @@ class CashSeeder extends Seeder
{ {
public function run(): void public function run(): void
{ {
$pemasukan = CashCategory::firstOrCreate(['name' => 'pemasukan']); $pemasukan = CashCategory::firstOrCreate(['name' => 'Iuran & Donasi'], ['type' => 'pemasukan']);
$pengeluaran = CashCategory::firstOrCreate(['name' => 'pengeluaran']); $pengeluaran = CashCategory::firstOrCreate(['name' => 'Operasional'], ['type' => 'pengeluaran']);
$bendahara = User::role('bendahara')->first(); $bendahara = User::role('bendahara')->first();
$ketua = User::role('ketua')->first(); $ketua = User::role('ketua')->first();
@@ -26,11 +27,30 @@ class CashSeeder extends Seeder
]; ];
foreach ($records as $data) { foreach ($records as $data) {
CashRecord::create(array_merge($data, [ $amount = $data['amount'];
'created_by' => $bendahara?->id,
'verified_by' => $ketua?->id, // 500rb2jt: butuh approval ketua, belum verified
'verified_at' => now()->subDays(1), if ($amount >= 500_000 && $amount <= 2_000_000) {
])); $record = CashRecord::create(array_merge($data, [
'created_by' => $bendahara?->id,
'verified_by' => null,
'verified_at' => null,
]));
Approval::create([
'model_type' => CashRecord::class,
'model_id' => $record->id,
'required_approvals' => 1,
'status' => 'pending',
]);
} else {
// < 500rb: langsung verified
CashRecord::create(array_merge($data, [
'created_by' => $bendahara?->id,
'verified_by' => $ketua?->id,
'verified_at' => now()->subDays(1),
]));
}
} }
} }
} }
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Database\Seeders;
use App\Models\ContactMessage;
use Illuminate\Database\Seeder;
class ContactMessageSeeder extends Seeder
{
public function run(): void
{
$messages = [
['name' => 'Budi Santoso', 'email' => 'budi@example.com', 'phone' => '08123456789', 'subject' => 'Pendaftaran anggota baru', 'message' => 'Saya ingin bergabung dengan organisasi Persegi. Bagaimana cara mendaftarnya?'],
['name' => 'Siti Rahayu', 'email' => null, 'phone' => '08987654321', 'subject' => 'Informasi kegiatan kerja bakti', 'message' => 'Apakah masyarakat umum boleh ikut kegiatan kerja bakti yang akan datang?'],
['name' => 'Ahmad Fauzi', 'email' => 'ahmad@example.com', 'phone' => null, 'subject' => 'Donasi untuk kegiatan', 'message' => 'Saya ingin berdonasi untuk mendukung kegiatan. Kemana saya bisa menghubungi bendahara?'],
['name' => 'Dewi Lestari', 'email' => 'dewi@example.com', 'phone' => '08111222333', 'subject' => 'Jadwal rapat bulanan', 'message' => 'Kapan jadwal rapat bulanan berikutnya? Saya ingin hadir sebagai tamu.'],
['name' => 'Hendra Wijaya', 'email' => null, 'phone' => '08555666777', 'subject' => 'Laporan kerusakan fasilitas desa', 'message' => 'Ada lampu jalan yang mati di RT 03. Apakah Persegi bisa membantu melaporkan ke desa?'],
['name' => 'Rina Kusuma', 'email' => 'rina@example.com', 'phone' => '08222333444', 'subject' => 'Kerjasama kegiatan sosial', 'message' => 'Kami dari karang taruna RT 05 ingin mengajak kerjasama untuk kegiatan sosial bulan depan.'],
['name' => 'Joko Pramono', 'email' => null, 'phone' => '08444555666', 'subject' => 'Pertanyaan iuran anggota', 'message' => 'Berapa besaran iuran bulanan anggota Persegi? Saya tertarik untuk bergabung.'],
['name' => 'Nurul Hidayah', 'email' => 'nurul@example.com', 'phone' => '08777888999', 'subject' => 'Saran program kerja', 'message' => 'Saya punya usulan program pelatihan digital untuk pemuda desa. Bagaimana cara menyampaikannya?'],
];
foreach ($messages as $data) {
ContactMessage::create($data);
}
}
}
+2
View File
@@ -18,6 +18,8 @@ class DatabaseSeeder extends Seeder
VoteSeeder::class, VoteSeeder::class,
PostSeeder::class, PostSeeder::class,
AuditSeeder::class, AuditSeeder::class,
MemberDueSeeder::class,
ContactMessageSeeder::class,
MemberPointSeeder::class, MemberPointSeeder::class,
]); ]);
} }
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Database\Seeders;
use App\Models\MemberDue;
use App\Models\User;
use Illuminate\Database\Seeder;
class MemberDueSeeder extends Seeder
{
public function run(): void
{
$bendahara = User::role('bendahara')->first();
$members = User::whereDoesntHave('roles', fn ($q) => $q->whereIn('name', ['super_admin', 'bendahara']))
->get();
$periods = [
now()->subMonths(3)->format('Y-m'),
now()->subMonths(2)->format('Y-m'),
now()->subMonth()->format('Y-m'),
now()->format('Y-m'),
];
foreach ($members as $i => $user) {
foreach ($periods as $j => $period) {
MemberDue::firstOrCreate(
['user_id' => $user->id, 'period' => $period],
[
'amount' => 10000,
'status' => ($i + $j) % 3 === 0 ? 'belum' : 'lunas',
'created_by' => $bendahara?->id,
]
);
}
}
}
}
+5 -5
View File
@@ -16,17 +16,17 @@ class MemberPointSeeder extends Seeder
Activity::whereNotNull('executed_at')->each(function ($activity) { Activity::whereNotNull('executed_at')->each(function ($activity) {
$activity->participants()->wherePivot('status', 'hadir')->each(function ($user) use ($activity) { $activity->participants()->wherePivot('status', 'hadir')->each(function ($user) use ($activity) {
MemberPoint::firstOrCreate( 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}"] ['points' => 10, 'reason' => "Hadir di kegiatan: {$activity->title}"]
); );
}); });
}); });
// Poin dari artikel yang sudah published // Poin dari artikel yang sudah approved
Post::where('status', 'published')->each(function ($post) { Post::where('status', 'approved')->orWhere('status', 'published')->each(function ($post) {
MemberPoint::firstOrCreate( MemberPoint::firstOrCreate(
['user_id' => $post->author_id, 'source_type' => 'post', 'source_id' => $post->id], ['user_id' => $post->author_id, 'source_type' => Post::class, 'source_id' => $post->id],
['points' => 5, 'reason' => "Artikel dipublikasi: {$post->title}"] ['points' => 5, 'reason' => "Artikel disetujui: {$post->title}"]
); );
}); });
} }
+8 -1
View File
@@ -13,7 +13,7 @@ class PermissionSeeder extends Seeder
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
// Buat roles jika belum ada // 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']); Role::firstOrCreate(['name' => $role, 'guard_name' => 'web']);
} }
@@ -28,6 +28,7 @@ class PermissionSeeder extends Seeder
$pengurus = Role::findByName('pengurus'); $pengurus = Role::findByName('pengurus');
$anggota = Role::findByName('anggota'); $anggota = Role::findByName('anggota');
$auditor = Role::findByName('auditor'); $auditor = Role::findByName('auditor');
$editor = Role::findByName('editor');
$ketua->syncPermissions(Permission::where('name', 'not like', '%Role%') $ketua->syncPermissions(Permission::where('name', 'not like', '%Role%')
->where('name', 'not like', '%Permission%') ->where('name', 'not like', '%Permission%')
@@ -63,5 +64,11 @@ class PermissionSeeder extends Seeder
->orWhere('name', 'like', 'View:%') ->orWhere('name', 'like', 'View:%')
->orWhere('name', 'like', '%Audit%') ->orWhere('name', 'like', '%Audit%')
->get()); ->get());
$editor->syncPermissions(Permission::whereIn('name', [
'ViewAny:Post', 'View:Post', 'Create:Post', 'Update:Post', 'Delete:Post',
'DeleteAny:Post', 'ViewAny:Activity', 'View:Activity',
'Publish:Post',
])->get());
} }
} }
+52 -3
View File
@@ -5,38 +5,87 @@ namespace Database\Seeders;
use App\Models\Post; use App\Models\Post;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
class PostSeeder extends Seeder class PostSeeder extends Seeder
{ {
public function run(): void 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 = [ $posts = [
// Published — sudah melalui full workflow
[ [
'title' => 'Selamat Datang di Website Persegi', 'title' => 'Selamat Datang di Website Persegi',
'category' => 'pengumuman', '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>', '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), 'published_at' => now()->subDays(10),
], ],
[ [
'title' => 'Rekrutmen Anggota Baru 2026', 'title' => 'Rekrutmen Anggota Baru 2026',
'category' => 'pengumuman', '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>', '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), 'published_at' => now()->subDays(5),
], ],
[ [
'title' => 'Laporan Kegiatan Kerja Bakti Desa', 'title' => 'Laporan Kegiatan Kerja Bakti Desa',
'category' => 'berita', '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>', '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), '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) { foreach ($posts as $data) {
Post::firstOrCreate( Post::firstOrCreate(
['slug' => \Illuminate\Support\Str::slug($data['title'])], ['slug' => Str::slug($data['title'])],
array_merge($data, ['author_id' => $author->id, 'status' => 'published']) $data
); );
} }
} }
+9
View File
@@ -27,6 +27,15 @@ class UserSeeder extends Seeder
->each(fn ($user) => $user->assignRole($role)); ->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 // 2 user tanpa role
User::factory(2)->create(['division_id' => fake()->randomElement($divisions)]); User::factory(2)->create(['division_id' => fake()->randomElement($divisions)]);
} }
+1912
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -7,11 +7,11 @@
"dev": "vite" "dev": "vite"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.2.2",
"axios": ">=1.11.0 <=1.14.0", "axios": ">=1.11.0 <=1.14.0",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"laravel-vite-plugin": "^3.0.0", "laravel-vite-plugin": "^3.0.0",
"tailwindcss": "^4.0.0", "tailwindcss": "^4.2.2",
"vite": "^8.0.0" "vite": "^8.0.0"
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

+15 -2
View File
@@ -6,6 +6,19 @@
@source '../**/*.js'; @source '../**/*.js';
@theme { @theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', --font-sans: 'Roboto', ui-sans-serif, system-ui, sans-serif;
'Segoe UI Symbol', 'Noto Color Emoji'; --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; }
+4
View File
@@ -0,0 +1,4 @@
@import '../../../../vendor/filament/filament/resources/css/theme.css';
@source '../../../../app/Filament/**/*';
@source '../../../../resources/views/filament/**/*';
+1 -23
View File
@@ -4,32 +4,10 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Persegi') Organisasi Pemuda Desa Karangdadap</title> <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.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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"> <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> @vite('resources/css/app.css')
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>
</head> </head>
<body class="bg-white text-gray-900 font-sans antialiased" style="font-family: 'Roboto', sans-serif;"> <body class="bg-white text-gray-900 font-sans antialiased" style="font-family: 'Roboto', sans-serif;">
+4 -1
View File
@@ -1,16 +1,19 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin'; import laravel from 'laravel-vite-plugin';
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite';
import os from 'os';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
laravel({ 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, refresh: true,
}), }),
tailwindcss(), tailwindcss(),
], ],
server: { server: {
host: os.networkInterfaces().eth0?.[0].address,
cors: true,
watch: { watch: {
ignored: ['**/storage/framework/views/**'], ignored: ['**/storage/framework/views/**'],
}, },