├── public ├── favicon.ico ├── robots.txt ├── js │ ├── eightynine │ │ └── filament-advanced-widgets │ │ │ └── filament-advanced-widgets-scripts.js │ └── filament │ │ ├── forms │ │ └── components │ │ │ ├── textarea.js │ │ │ ├── tags-input.js │ │ │ └── key-value.js │ │ └── tables │ │ └── components │ │ └── table.js ├── index.php ├── css │ ├── filipfonal │ │ └── filament-log-manager │ │ │ └── filament-log-manager.css │ ├── awcodes │ │ └── palette │ │ │ └── palette-select-styles.css │ └── filament │ │ └── support │ │ └── support.css └── .htaccess ├── database ├── sqlite │ └── database.sqlite ├── seeders │ ├── UserSeeder.php │ ├── ZakahPaymentSeeder.php │ ├── ExchangePriceSeeder.php │ ├── GoldSeeder.php │ ├── MoneySeeder.php │ ├── SilverSeeder.php │ └── DatabaseSeeder.php ├── factories │ ├── CurrencyFactory.php │ ├── ExchangePriceFactory.php │ ├── MoneyFactory.php │ ├── ZakahPaymentFactory.php │ ├── GoldFactory.php │ ├── SilverFactory.php │ └── UserFactory.php └── migrations │ ├── 2025_01_05_190539_create_exchange_prices_table.php │ ├── 2025_01_05_171444_create_currencies_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 2025_01_05_171445_create_money_table.php │ ├── 2025_03_25_064612_create_filament-jobs-monitor_table.php │ ├── 2025_01_05_190204_add_total_zakah_values_to_users_table.php │ ├── 2025_03_25_063525_create_health_tables.php │ ├── 2025_04_27_185324_create_zakah_payments_table.php │ ├── 2025_01_19_154948_create_silvers_table.php │ ├── 2025_01_18_115429_create_gold_table.php │ ├── 0001_01_01_000000_create_users_table.php │ └── 0001_01_01_000002_create_jobs_table.php ├── bootstrap ├── cache │ └── .gitignore ├── providers.php └── app.php ├── resources ├── js │ ├── app.js │ └── bootstrap.js ├── css │ └── app.css └── views │ ├── filament │ ├── pages │ │ └── auth │ │ │ └── register.blade.php │ └── widgets │ │ └── title.blade.php │ └── mail │ └── ResetMoney.blade.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── private │ │ └── .gitignore │ ├── public │ │ └── .gitignore │ └── .gitignore ├── debugbar │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── sessions │ └── .gitignore │ ├── cache │ └── .gitignore │ └── .gitignore ├── extra └── palestine.png ├── postcss.config.js ├── app ├── Observers │ ├── ZakahPaymentObserver.php │ ├── SilverObserver.php │ ├── GoldObserver.php │ ├── MoneyObserver.php │ └── UserObserver.php ├── Http │ ├── Controllers │ │ ├── Controller.php │ │ ├── ZakahPaymentController.php │ │ └── HandleStorageController.php │ └── Requests │ │ ├── StoreZakahPaymentRequest.php │ │ └── UpdateZakahPaymentRequest.php ├── Filament │ ├── Widgets │ │ ├── TotalAssets.php │ │ ├── TitleWidget.php │ │ ├── PricesToday.php │ │ ├── MoneyAssetsOverview.php │ │ └── MoneyAssetsZakahOverview.php │ ├── Resources │ │ ├── CurrencyResource │ │ │ └── Pages │ │ │ │ ├── CreateCurrency.php │ │ │ │ ├── ListCurrencies.php │ │ │ │ └── EditCurrency.php │ │ ├── ExchangePriceResource │ │ │ └── Pages │ │ │ │ ├── CreateExchangePrice.php │ │ │ │ ├── ListExchangePrices.php │ │ │ │ └── EditExchangePrice.php │ │ ├── UserResource │ │ │ └── Pages │ │ │ │ ├── EditUser.php │ │ │ │ ├── ListUsers.php │ │ │ │ └── CreateUser.php │ │ ├── MoneyResource │ │ │ ├── Pages │ │ │ │ ├── CreateMoney.php │ │ │ │ ├── ListMoney.php │ │ │ │ └── EditMoney.php │ │ │ └── Widgets │ │ │ │ └── MoneyResourceOverview.php │ │ ├── ZakahPaymentResource │ │ │ └── Pages │ │ │ │ ├── CreateZakahPayment.php │ │ │ │ ├── EditZakahPayment.php │ │ │ │ └── ListZakahPayments.php │ │ ├── SilverResource │ │ │ ├── Pages │ │ │ │ ├── ListSilvers.php │ │ │ │ ├── CreateSilver.php │ │ │ │ └── EditSilver.php │ │ │ └── Widgets │ │ │ │ └── SilverResourceOverview.php │ │ ├── GoldResource │ │ │ ├── Pages │ │ │ │ ├── ListGold.php │ │ │ │ ├── CreateGold.php │ │ │ │ └── EditGold.php │ │ │ └── Widgets │ │ │ │ └── GoldResourceOverview.php │ │ ├── ExchangePriceResource.php │ │ └── CurrencyResource.php │ └── Pages │ │ └── Auth │ │ ├── Login.php │ │ └── Register.php ├── Enums │ ├── UserRoleEnum.php │ ├── MoneyTypeEnum.php │ ├── KaratEnum.php │ ├── WeightEnum.php │ ├── ZakahPaymentStatusEnum.php │ ├── ZakahPaymentTypeEnum.php │ ├── PreciousMetalTypeEnum.php │ ├── ZakahPaymentMethodEnum.php │ └── PreciousMetalColorEnum.php ├── Traits │ └── EnumToArray.php ├── Models │ ├── Scopes │ │ └── OwnRecordScope.php │ ├── ExchangePrice.php │ ├── Currency.php │ ├── Money.php │ ├── ZakahPayment.php │ ├── Silver.php │ ├── Gold.php │ └── User.php ├── Casts │ └── MoneyCast.php ├── Jobs │ ├── SendZakahReminderBeforeThirtyDaysJob.php │ └── SendZakahReminderOnPaymentDay.php ├── Console │ └── Commands │ │ ├── RefreshAllExchangePricesCommand.php │ │ ├── RefreshZakahValues.php │ │ ├── GetExchangePriceTodayCommand.php │ │ ├── GetGoldPriceTodayCommand.php │ │ ├── GetSilverPriceTodayCommand.php │ │ └── UpdateAllPricesToUsdCommand.php ├── Mail │ ├── SendZakahReminderMail.php │ └── WelcomeMail.php ├── Helpers │ └── CacheHelper.php └── Providers │ ├── AppServiceProvider.php │ └── Filament │ └── AdminPanelProvider.php ├── config ├── settings.php ├── filament-jobs-monitor.php ├── filament-log-manager.php ├── services.php ├── filesystems.php ├── cache.php ├── mail.php └── queue.php ├── tests ├── TestCase.php └── Unit │ ├── ScheduleTest.php │ ├── ZakahResetsIfAmountLessThanNisabTest.php │ ├── ZakahReminderTest.php │ ├── UserCantAccessOtherUserDataTest.php │ ├── GoldZakahPaymentTest.php │ └── SilverZakahPaymentTest.php ├── .gitattributes ├── vite.config.js ├── .editorconfig ├── routes ├── web.php └── console.php ├── .gitignore ├── artisan ├── package.json ├── .dockerignore ├── deploy.sh ├── lang ├── ar │ ├── pagination.php │ ├── auth.php │ ├── passwords.php │ └── general.php ├── en │ ├── pagination.php │ ├── auth.php │ ├── passwords.php │ └── general.php └── vendor │ └── filament-log-manager │ ├── ar │ └── translations.php │ ├── en │ └── translations.php │ ├── id │ └── translations.php │ ├── it │ └── translations.php │ ├── fr │ └── translations.php │ └── de │ └── translations.php ├── tailwind.config.js ├── docker ├── entrypoint.sh └── base_supervisord.conf ├── docker-compose.yaml ├── README.md ├── Dockerfile ├── phpunit.xml ├── .env.example └── composer.json /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/sqlite/database.sqlite: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import './bootstrap'; 2 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /storage/app/private/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/debugbar/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !private/ 3 | !public/ 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /extra/palestine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cybrarist/Zakaty/HEAD/extra/palestine.png -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /resources/views/filament/pages/auth/register.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /app/Observers/ZakahPaymentObserver.php: -------------------------------------------------------------------------------- 1 | env("EXCHANGE_RATE_API_KEY"), 5 | 6 | 'precious_metal_price_api_key' => env("PRECIOUS_METAL_PRICE_API_KEY"), 7 | 8 | ]; 9 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | name('storage.local') 7 | ->middleware('auth'); 8 | 9 | Route::get('/temp', function (){ 10 | return redirect()->route('filament.admin.auth.login'); 11 | })->name('login'); 12 | -------------------------------------------------------------------------------- /database/seeders/UserSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /database/seeders/ZakahPaymentSeeder.php: -------------------------------------------------------------------------------- 1 | name); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Filament/Resources/CurrencyResource/Pages/CreateCurrency.php: -------------------------------------------------------------------------------- 1 | 4 | {{$user->next_money_zakah_date}} 5 |

6 | 7 | 8 |

9 | Click This Button To Reset 10 |

11 | -------------------------------------------------------------------------------- /app/Filament/Widgets/TitleWidget.php: -------------------------------------------------------------------------------- 1 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /app/Filament/Resources/ExchangePriceResource/Pages/CreateExchangePrice.php: -------------------------------------------------------------------------------- 1 | createQuietly(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /database/seeders/MoneySeeder.php: -------------------------------------------------------------------------------- 1 | createQuietly(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | dailyAt('12:00'); 8 | 9 | 10 | Schedule::job(SendZakahReminderBeforeThirtyDaysJob::class)->dailyAt('12:00'); 11 | Schedule::job(SendZakahReminderOnPaymentDay::class)->dailyAt('12:00'); 12 | -------------------------------------------------------------------------------- /database/seeders/SilverSeeder.php: -------------------------------------------------------------------------------- 1 | createQuietly(); 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "vite build", 6 | "dev": "vite" 7 | }, 8 | "devDependencies": { 9 | "autoprefixer": "^10.4.20", 10 | "axios": "^1.7.4", 11 | "concurrently": "^9.0.1", 12 | "laravel-vite-plugin": "^1.2.0", 13 | "postcss": "^8.4.47", 14 | "tailwindcss": "^3.4.13", 15 | "vite": "^6.0.11" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Traits/EnumToArray.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

5 | {{$title}} 6 |

7 |
8 |
9 |
10 |
11 | 12 | -------------------------------------------------------------------------------- /app/Filament/Resources/UserResource/Pages/EditUser.php: -------------------------------------------------------------------------------- 1 | where('user_id', Auth::id()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /database/factories/CurrencyFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class CurrencyFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition(): array 18 | { 19 | return [ 20 | // 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 18 | -------------------------------------------------------------------------------- /app/Filament/Resources/CurrencyResource/Pages/ListCurrencies.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ExchangePriceFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition(): array 18 | { 19 | return [ 20 | // 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Enums/MoneyTypeEnum.php: -------------------------------------------------------------------------------- 1 | name); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/Models/ExchangePrice.php: -------------------------------------------------------------------------------- 1 | */ 11 | use HasFactory; 12 | 13 | protected $fillable=[ 14 | "name", 15 | "value", 16 | ]; 17 | 18 | protected function casts(): array 19 | { 20 | return [ 21 | "value" => \App\Casts\MoneyCast::class 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Filament/Resources/ExchangePriceResource/Pages/ListExchangePrices.php: -------------------------------------------------------------------------------- 1 | withRouting( 9 | web: __DIR__.'/../routes/web.php', 10 | commands: __DIR__.'/../routes/console.php', 11 | health: '/up', 12 | ) 13 | ->withMiddleware(function (Middleware $middleware) { 14 | // 15 | }) 16 | ->withExceptions(function (Exceptions $exceptions) { 17 | // 18 | })->create(); 19 | -------------------------------------------------------------------------------- /app/Enums/KaratEnum.php: -------------------------------------------------------------------------------- 1 | value . ' Karats'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | image="cybrarist/zakaty" 4 | version=$1 5 | 6 | 7 | php artisan migrate:fresh --force --seed 8 | 9 | rm storage/debugbar/* 10 | rm storage/views/* 11 | rm -r storage/framework/cache/* 12 | rm storage/framework/sessions/* 13 | rm storage/logs/* 14 | 15 | php artisan exchange:price 16 | php artisan gold:price 17 | php artisan silver:price 18 | 19 | docker build --platform linux/amd64,linux/arm64 -t "$image:v$version" . 20 | docker build --platform linux/amd64,linux/arm64 -t "$image:latest" . 21 | 22 | 23 | 24 | docker push "$image:v$version" 25 | docker push "$image:latest" 26 | -------------------------------------------------------------------------------- /lang/ar/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /app/Filament/Pages/Auth/Login.php: -------------------------------------------------------------------------------- 1 | to('/register'); 19 | } 20 | 21 | parent::mount(); // TODO: Change the autogenerated stub 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Filament/Resources/UserResource/Pages/CreateUser.php: -------------------------------------------------------------------------------- 1 | currency_id; 18 | 19 | return $data; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'tailwindcss/defaultTheme'; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: [ 6 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 7 | './storage/framework/views/*.php', 8 | './resources/**/*.blade.php', 9 | './resources/**/*.js', 10 | './resources/**/*.vue', 11 | ], 12 | theme: { 13 | extend: { 14 | fontFamily: { 15 | sans: ['Figtree', ...defaultTheme.fontFamily.sans], 16 | }, 17 | }, 18 | }, 19 | plugins: [], 20 | }; 21 | -------------------------------------------------------------------------------- /config/filament-jobs-monitor.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'enabled' => true, 6 | 'label' => 'Job', 7 | 'plural_label' => 'Jobs', 8 | 'navigation_group' => 'Settings', 9 | 'navigation_icon' => 'heroicon-o-cpu-chip', 10 | 'navigation_sort' => null, 11 | 'navigation_count_badge' => false, 12 | 'resource' => Croustibat\FilamentJobsMonitor\Resources\QueueMonitorResource::class, 13 | 'cluster' => null, 14 | ], 15 | 'pruning' => [ 16 | 'enabled' => true, 17 | 'retention_days' => 7, 18 | ], 19 | 'queues' => [ 20 | 'default', 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/textarea.js: -------------------------------------------------------------------------------- 1 | function r({initialHeight:t,shouldAutosize:i,state:s}){return{state:s,wrapperEl:null,init:function(){this.wrapperEl=this.$el.parentNode,this.setInitialHeight(),i?this.$watch("state",()=>{this.resize()}):this.setUpResizeObserver()},setInitialHeight:function(){this.$el.scrollHeight<=0||(this.wrapperEl.style.height=t+"rem")},resize:function(){if(this.setInitialHeight(),this.$el.scrollHeight<=0)return;let e=this.$el.scrollHeight+"px";this.wrapperEl.style.height!==e&&(this.wrapperEl.style.height=e)},setUpResizeObserver:function(){new ResizeObserver(()=>{this.wrapperEl.style.height=this.$el.style.height}).observe(this.$el)}}}export{r as default}; 2 | -------------------------------------------------------------------------------- /lang/vendor/filament-log-manager/ar/translations.php: -------------------------------------------------------------------------------- 1 | 'السجلات', 5 | 'navigation_group' => 'النظام', 6 | 'title' => 'السجلات', 7 | 'search_placeholder' => 'اختر أو ابحث عن سجل', 8 | 'no_logs' => 'لا توجد سجلات لعرضها', 9 | 'delete' => 'حذف', 10 | 'download' => 'تحميل', 11 | 'no_such_file' => 'هذا الملف غير موجود', 12 | 'file_too_large' => 'الملف كبير جداً', 13 | 'modal_delete_heading' => 'هل تريد حذف هذا السجل؟', 14 | 'modal_delete_subheading' => 'هل أنت متأكد أنك تريد حذف هذا السجل؟', 15 | 'modal_delete_action_cancel' => 'إلغاء', 16 | 'modal_delete_action_confirm' => 'حذف', 17 | ]; 18 | -------------------------------------------------------------------------------- /lang/vendor/filament-log-manager/en/translations.php: -------------------------------------------------------------------------------- 1 | 'Logs', 5 | 'navigation_group' => 'Settings', 6 | 'title' => 'Logs', 7 | 'search_placeholder' => 'Select or search a log file', 8 | 'no_logs' => 'No Logs to display', 9 | 'delete' => 'Delete', 10 | 'download' => 'Download', 11 | 'no_such_file' => 'No such file', 12 | 'file_too_large' => 'File is too large', 13 | 'modal_delete_heading' => 'Delete this logs file?', 14 | 'modal_delete_subheading' => 'Are you sure you want to delete this logs file?', 15 | 'modal_delete_action_cancel' => 'Cancel', 16 | 'modal_delete_action_confirm' => 'Delete', 17 | ]; 18 | -------------------------------------------------------------------------------- /app/Filament/Resources/MoneyResource/Pages/CreateMoney.php: -------------------------------------------------------------------------------- 1 | rate; 19 | return $data; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Filament/Resources/CurrencyResource/Pages/EditCurrency.php: -------------------------------------------------------------------------------- 1 | |string> 21 | */ 22 | public function rules(): array 23 | { 24 | return [ 25 | // 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lang/vendor/filament-log-manager/id/translations.php: -------------------------------------------------------------------------------- 1 | 'Log', 5 | 'navigation_group' => 'Sistem', 6 | 'title' => 'Log', 7 | 'search_placeholder' => 'Pilih atau cari berkas log', 8 | 'no_logs' => 'Tidak ada log untuk ditampilkan', 9 | 'delete' => 'Hapus', 10 | 'download' => 'Unduh', 11 | 'no_such_file' => 'Berkas tidak ditemukan', 12 | 'file_too_large' => 'Ukuran berkas terlalu besar', 13 | 'modal_delete_heading' => 'Hapus berkas log ini?', 14 | 'modal_delete_subheading' => 'Apakah Anda yakin untuk menghapus berkas log ini?', 15 | 'modal_delete_action_cancel' => 'Batal', 16 | 'modal_delete_action_confirm' => 'Hapus', 17 | ]; 18 | -------------------------------------------------------------------------------- /app/Filament/Resources/MoneyResource/Pages/ListMoney.php: -------------------------------------------------------------------------------- 1 | rate; 18 | return $data; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Requests/UpdateZakahPaymentRequest.php: -------------------------------------------------------------------------------- 1 | |string> 21 | */ 22 | public function rules(): array 23 | { 24 | return [ 25 | // 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Filament/Resources/SilverResource/Pages/ListSilvers.php: -------------------------------------------------------------------------------- 1 | 'Logs', 5 | 'navigation_group' => 'System', 6 | 'title' => 'Logs', 7 | 'search_placeholder' => 'Seleziona o cerca un file di log', 8 | 'no_logs' => 'Nessun file di log', 9 | 'delete' => 'Elimina', 10 | 'download' => 'Download', 11 | 'no_such_file' => 'Nessun file di log con questo nome', 12 | 'file_too_large' => 'File troppo grande per essere visualizzato', 13 | 'modal_delete_heading' => 'Elimina file di log?', 14 | 'modal_delete_subheading' => 'Sei sicuro di voler eliminare questo file di log?', 15 | 'modal_delete_action_cancel' => 'Annulla', 16 | 'modal_delete_action_confirm' => 'Elimina', 17 | ]; 18 | -------------------------------------------------------------------------------- /lang/vendor/filament-log-manager/fr/translations.php: -------------------------------------------------------------------------------- 1 | 'Logs', 5 | 'navigation_group' => 'Système', 6 | 'title' => 'Logs', 7 | 'search_placeholder' => 'Sélectionner ou rechercher un fichier de log', 8 | 'no_logs' => 'Aucun Logs à afficher', 9 | 'delete' => 'Supprimer', 10 | 'download' => 'Télécharger', 11 | 'no_such_file' => "Ce fichier n'existe pas", 12 | 'file_too_large' => 'Fichier trop gros', 13 | 'modal_delete_heading' => 'Supprimer ce fichier de logs?', 14 | 'modal_delete_subheading' => 'Etes-vous certain de vouloir supprimer ce fichier de Logs?', 15 | 'modal_delete_action_cancel' => 'Annuler', 16 | 'modal_delete_action_confirm' => 'Supprimer', 17 | ]; 18 | -------------------------------------------------------------------------------- /lang/ar/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'password' => 'The provided password is incorrect.', 18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'password' => 'The provided password is incorrect.', 18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -f "vendor/autoload.php" ]; then 4 | echo "Installing Composer" 5 | composer install --no-interaction --no-progress 6 | else 7 | composer dump-autoload 8 | fi 9 | 10 | if [ ! -f "/logs" ]; then 11 | mkdir /logs 12 | fi 13 | 14 | cp .env.example .env 15 | 16 | php artisan key:generate --force 17 | 18 | printenv > /etc/environment 19 | 20 | php artisan migrate --force --seed 21 | 22 | #clear cache 23 | php artisan filament:optimize-clear 24 | php artisan optimize:clear 25 | 26 | php artisan exchange:refresh-all 27 | 28 | php artisan optimize 29 | php artisan filament:optimize 30 | 31 | php artisan octane:install --server=frankenphp 32 | 33 | supervisord -c /etc/supervisor/conf.d/supervisord.conf 34 | 35 | -------------------------------------------------------------------------------- /lang/vendor/filament-log-manager/de/translations.php: -------------------------------------------------------------------------------- 1 | 'Protokolle', 5 | 'navigation_group' => 'System', 6 | 'title' => 'Protokolle', 7 | 'search_placeholder' => 'Wählen oder suchen Sie eine Protokolldatei', 8 | 'no_logs' => 'Keine Protokolle zum Anzeigen', 9 | 'delete' => 'Löschen', 10 | 'download' => 'Herunterladen', 11 | 'no_such_file' => 'Keine solche Datei', 12 | 'file_too_large' => 'Datei ist zu groß', 13 | 'modal_delete_heading' => 'Diese Protokolldatei löschen?', 14 | 'modal_delete_subheading' => 'Sind Sie sicher, dass Sie diese Protokolldatei löschen möchten?', 15 | 'modal_delete_action_cancel' => 'Abbrechen', 16 | 'modal_delete_action_confirm' => 'Löschen', 17 | ]; 18 | -------------------------------------------------------------------------------- /app/Filament/Resources/GoldResource/Pages/ListGold.php: -------------------------------------------------------------------------------- 1 | name); 22 | } 23 | 24 | public static function get_weight_in_gram($value, $weight): float|int 25 | { 26 | 27 | return match ($value) { 28 | WeightEnum::KiloGram => $weight * 1000, 29 | WeightEnum::Ounce => $weight * 28.35, 30 | WeightEnum::Pound => $weight * 453.6, 31 | default => $weight 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | zakaty: 3 | image: cybrarist/zakaty:latest 4 | # build: 5 | # context: . 6 | ports: 7 | - 8080:80 8 | environment: 9 | DB_CONNECTION: sqlite 10 | # DB_HOST=127.0.0.1 11 | # DB_PORT=3306 12 | # DB_DATABASE=zakah 13 | # DB_USERNAME=root 14 | # DB_PASSWORD= 15 | APP_TIMEZONE: Asia/Dubai 16 | APP_URL: "http://localhost:8080" 17 | ASSET_URL: "http://localhost:8080" 18 | MAIL_MAILER: smtp 19 | MAIL_HOST: smtp.gmail.com 20 | MAIL_PORT: 465 21 | MAIL_USERNAME: "email@gmail.com" 22 | MAIL_PASSWORD: "app password" 23 | MAIL_ENCRYPTION: tls 24 | MAIL_FROM_ADDRESS: "email@gmail.com" 25 | EXCHANGE_RATE_API_KEY: "" 26 | PRECIOUS_METAL_PRICE_API_KEY: "" 27 | 28 | -------------------------------------------------------------------------------- /app/Filament/Resources/ZakahPaymentResource/Pages/EditZakahPayment.php: -------------------------------------------------------------------------------- 1 | rate; 24 | return $data; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/migrations/2025_01_05_190539_create_exchange_prices_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->timestamps(); 17 | 18 | $table->string("name")->unique(); 19 | $table->unsignedInteger("value"); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('exchange_prices'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /app/Casts/MoneyCast.php: -------------------------------------------------------------------------------- 1 | $attributes 14 | */ 15 | public function get(Model $model, string $key, mixed $value, array $attributes): mixed 16 | { 17 | return round(floatval($value) / 1000, precision: 3); 18 | } 19 | 20 | /** 21 | * Prepare the given value for storage. 22 | * 23 | * @param array $attributes 24 | */ 25 | public function set(Model $model, string $key, mixed $value, array $attributes): mixed 26 | { 27 | return round(floatval($value) * 1000); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lang/ar/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset.', 17 | 'sent' => 'We have emailed your password reset link.', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset.', 17 | 'sent' => 'We have emailed your password reset link.', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /tests/Unit/ScheduleTest.php: -------------------------------------------------------------------------------- 1 | make(\Illuminate\Console\Scheduling\Schedule::class); 14 | 15 | $events = collect($schedule->events())->filter(function ($event) { 16 | return Str::contains($event->command, 'exchange:refresh-all'); 17 | }); 18 | 19 | $this->assertNotEmpty($events, 'The RefreshAllExchangePricesCommand is not scheduled'); 20 | 21 | $events->each(function ($event) { 22 | $this->assertEquals('0 12 * * *', $event->expression, 'Command is not scheduled for 12:00 daily'); 23 | }); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Handle X-XSRF-Token Header 13 | RewriteCond %{HTTP:x-xsrf-token} . 14 | RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] 15 | 16 | # Redirect Trailing Slashes If Not A Folder... 17 | RewriteCond %{REQUEST_FILENAME} !-d 18 | RewriteCond %{REQUEST_URI} (.+)/$ 19 | RewriteRule ^ %1 [L,R=301] 20 | 21 | # Send Requests To Front Controller... 22 | RewriteCond %{REQUEST_FILENAME} !-d 23 | RewriteCond %{REQUEST_FILENAME} !-f 24 | RewriteRule ^ index.php [L] 25 | 26 | -------------------------------------------------------------------------------- /app/Filament/Resources/SilverResource/Pages/CreateSilver.php: -------------------------------------------------------------------------------- 1 | e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{"x-on:blur":"createTag()","x-model":"newTag","x-on:keydown"(t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},"x-on:paste"(){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default}; 2 | -------------------------------------------------------------------------------- /app/Filament/Resources/GoldResource/Pages/CreateGold.php: -------------------------------------------------------------------------------- 1 | addMonth())->get(); 26 | 27 | foreach ($users as $user) { 28 | Mail::to($user) 29 | ->queue(new SendZakahReminderMail($user, '30 Days')); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Filament/Resources/MoneyResource/Pages/EditMoney.php: -------------------------------------------------------------------------------- 1 | rate; 21 | return $data; 22 | } 23 | 24 | protected function getHeaderActions(): array 25 | { 26 | return [ 27 | Actions\DeleteAction::make(), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Filament/Resources/ZakahPaymentResource/Pages/ListZakahPayments.php: -------------------------------------------------------------------------------- 1 | get(); 30 | 31 | foreach ($users as $user) { 32 | Mail::to($user) 33 | ->queue(new SendZakahReminderMail($user, 'Today')); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Enums/ZakahPaymentStatusEnum.php: -------------------------------------------------------------------------------- 1 | name); 23 | } 24 | 25 | 26 | public function getColor(): string|array|null 27 | { 28 | return match ($this->value){ 29 | self::Paid->value => Color::Green, 30 | self::Pending->value => Color::Amber, 31 | self::Cancelled->value => Color::Red, 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2025_01_05_171444_create_currencies_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->timestamps(); 17 | 18 | 19 | $table->string("name"); 20 | $table->string("code"); 21 | $table->string("symbol")->nullable(); 22 | 23 | $table->unsignedInteger("rate")->default(0); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('currencies'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/factories/MoneyFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class MoneyFactory extends Factory 12 | { 13 | /** 14 | * Define the model's default state. 15 | * 16 | * @return array 17 | */ 18 | public function definition(): array 19 | { 20 | return [ 21 | 'name' => $this->faker->name(), 22 | 'currency_id' => $this->faker->numberBetween(1, 1), 23 | 'amount' => $this->faker->numberBetween(100, 100), 24 | 'usd_amount' => $this->faker->numberBetween(100, 100), 25 | 'type' => $this->faker->randomElement([MoneyTypeEnum::Cash->value]), 26 | 'user_id' => $this->faker->numberBetween(1, 1), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Models/Currency.php: -------------------------------------------------------------------------------- 1 | */ 13 | use HasFactory; 14 | 15 | protected $fillable=[ 16 | "name", 17 | "code", 18 | "symbol", 19 | "rate", 20 | ]; 21 | 22 | protected function casts(): array 23 | { 24 | return [ 25 | 'rate'=>MoneyCast::class 26 | ]; 27 | } 28 | 29 | 30 | protected function getCurrencySymbolAttribute(): string 31 | { 32 | return $this->symbol ?? $this->code; 33 | } 34 | 35 | protected function getCodeNameAttribute(): string{ 36 | return $this->code . " - " . $this->name; 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/Observers/SilverObserver.php: -------------------------------------------------------------------------------- 1 | user_id) ? Auth::user() : User::findOrFail($silver->user_id); 15 | ZakahHelper::update_silver_zakah_data_for_user($user_to_update); 16 | ZakahHelper::set_amount_and_date_for_zakah($user_to_update); 17 | } 18 | 19 | /** 20 | * Handle the Silver "deleted" event. 21 | */ 22 | public function deleted(Silver $silver): void 23 | { 24 | $user_to_update = (Auth::id() == $silver->user_id) ? Auth::user() : User::findOrFail($silver->user_id); 25 | ZakahHelper::update_silver_zakah_data_for_user($user_to_update); 26 | ZakahHelper::set_amount_and_date_for_zakah($user_to_update); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Console/Commands/RefreshAllExchangePricesCommand.php: -------------------------------------------------------------------------------- 1 | {let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(t)===0&&s(e)===0||this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows:function(t){let e=Alpine.raw(this.rows);this.rows=[];let s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.$nextTick(()=>{this.rows=e,this.updateState()})},updateRows:function(){if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{r as default}; 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![FreePalestine](./extra/palestine.png) 2 | 3 | # Zakaty 4 | Zakaty is an zakah tracker for your assets, so you don't have to keep calculating it everytime 5 | 6 | # Documentation 7 | Feel free to access the up-to-date documentation from [Here](https://zakaty.cybrarist.com) 8 | 9 | ## Deployment 10 | Feel free to refer to [Documentation](https://zakaty.cybrarist.com/) 11 | 12 | ## Connect 13 | If you are coming outside github or don't like to use it, feel free to join [Discord](https://discord.com/invite/h2VGyKtJe4). 14 | 15 | ## Docker 16 | Please check the docker repos to pull the image you prefer [Docker](https://hub.docker.com/r/cybrarist/zakaty) 17 | 18 | # Demo 19 | if you would like to test the application first, feel free to check it on 20 | [Zakah Tracker](https://zakahtracker.com) 21 | 22 | you will get 30 days for free. 23 | 24 | it costs around 11$ for the whole year, BUT if you can't pay for it and you want to use the app, contact me on discord and we can reach something out whether a free subscription or reduced price. 25 | -------------------------------------------------------------------------------- /app/Observers/GoldObserver.php: -------------------------------------------------------------------------------- 1 | user_id) ? Auth::user() : User::findOrFail($gold->user_id); 18 | ZakahHelper::update_gold_zakah_data_for_user($user_to_update); 19 | ZakahHelper::set_amount_and_date_for_zakah($user_to_update); 20 | 21 | } 22 | 23 | /** 24 | * Handle the Gold "deleted" event. 25 | */ 26 | public function deleted(Gold $gold): void 27 | { 28 | $user_to_update = (Auth::id() == $gold->user_id) ? Auth::user() : User::findOrFail($gold->user_id); 29 | ZakahHelper::update_gold_zakah_data_for_user($user_to_update); 30 | ZakahHelper::set_amount_and_date_for_zakah($user_to_update); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Enums/ZakahPaymentTypeEnum.php: -------------------------------------------------------------------------------- 1 | name); 23 | } 24 | 25 | public function getColor(): string|array|null 26 | { 27 | return match ($this->value){ 28 | self::Money->value => Color::Green, 29 | self::Animal->value => Color::Blue, 30 | self::Crops->value => Color::Red, 31 | }; 32 | } 33 | 34 | public static function get_colors(): array 35 | { 36 | return ['#16A34A', '#3b82f6', '#dc2626']; 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /app/Observers/MoneyObserver.php: -------------------------------------------------------------------------------- 1 | user_id) ? Auth::user() : User::findOrFail($money->user_id); 18 | ZakahHelper::update_money_zakah_data_for_user($user_to_update); 19 | ZakahHelper::set_amount_and_date_for_zakah($user_to_update); 20 | } 21 | 22 | /** 23 | * Handle the Money "deleted" event. 24 | */ 25 | public function deleted(Money $money): void 26 | { 27 | $user_to_update = (Auth::id() == $money->user_id) ? Auth::user() : User::findOrFail($money->user_id); 28 | ZakahHelper::update_money_zakah_data_for_user($user_to_update); 29 | ZakahHelper::set_amount_and_date_for_zakah($user_to_update); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Console/Commands/RefreshZakahValues.php: -------------------------------------------------------------------------------- 1 | argument('user')); 31 | 32 | ZakahHelper::update_money_zakah_data_for_user($user); 33 | ZakahHelper::update_gold_zakah_data_for_user($user); 34 | ZakahHelper::update_silver_zakah_data_for_user($user); 35 | ZakahHelper::set_amount_and_date_for_zakah($user); 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config/filament-log-manager.php: -------------------------------------------------------------------------------- 1 | true, 11 | 12 | /** 13 | * Navigation icon. 14 | */ 15 | 'navigation_icon' => 'heroicon-o-server', 16 | 17 | /** 18 | * The directory(ies) containing the log files. 19 | */ 20 | 'logs_directory' => storage_path('logs'), 21 | 22 | /** 23 | * Allow deleting logs from the user interface. 24 | */ 25 | 'allow_delete' => true, 26 | 27 | /** 28 | * Allow downloading logs from the user interface. 29 | */ 30 | 'allow_download' => true, 31 | 32 | /** 33 | * Allow set custom logs page class. 34 | */ 35 | 'page_class' => Logs::class, 36 | 37 | /** 38 | * Permission Name to allow viewing logs 39 | */ 40 | 'permissions' => [ 41 | 'view_logs' => 'view-logs', 42 | ], 43 | ]; 44 | -------------------------------------------------------------------------------- /database/factories/ZakahPaymentFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class ZakahPaymentFactory extends Factory 14 | { 15 | /** 16 | * Define the model's default state. 17 | * 18 | * @return array 19 | */ 20 | public function definition(): array 21 | { 22 | return [ 23 | 'amount' => $this->faker->numberBetween(100, 10000), 24 | 'usd_amount' => $this->faker->numberBetween(100, 10000), 25 | 'type' => $this->faker->randomElement(ZakahPaymentTypeEnum::values()), 26 | 'status' => $this->faker->randomElement(ZakahPaymentStatusEnum::values()), 27 | 'payment_method' => $this->faker->randomElement(ZakahPaymentMethodEnum::values()), 28 | 'user_id' => 1 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM dunglas/frankenphp:1.5.0-php8.4-bookworm 2 | 3 | LABEL authors="Cybrarist" 4 | 5 | ENV SERVER_NAME=":80" 6 | ENV FRANKENPHP_CONFIG="worker /app/public/index.php" 7 | ENV FRANKEN_HOST="localhost" 8 | 9 | 10 | RUN apt update && apt install -y supervisor \ 11 | libbz2-dev \ 12 | libzip-dev \ 13 | libmcrypt-dev \ 14 | libicu-dev \ 15 | gnupg \ 16 | ca-certificates \ 17 | libx11-xcb1 \ 18 | libxcomposite1 \ 19 | libxdamage1 \ 20 | libxrandr2 \ 21 | libatk1.0-0 \ 22 | libnspr4 \ 23 | && apt-get clean 24 | 25 | 26 | 27 | RUN install-php-extensions @composer 28 | 29 | RUN docker-php-ext-install pcntl \ 30 | opcache \ 31 | pdo_mysql \ 32 | pdo \ 33 | bz2 \ 34 | intl \ 35 | bcmath \ 36 | zip 37 | 38 | 39 | COPY ./docker/base_supervisord.conf /etc/supervisor/conf.d/supervisord.conf 40 | 41 | COPY . /app 42 | 43 | WORKDIR /app 44 | 45 | EXPOSE 80 443 2019 8080 46 | 47 | 48 | RUN chmod +x /app/* 49 | 50 | ENTRYPOINT ["docker/entrypoint.sh"] 51 | -------------------------------------------------------------------------------- /docker/base_supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | user=root 3 | nodaemon=true 4 | logfile=/logs/supervisord_stdout.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:octane] 8 | command=php artisan octane:frankenphp --host=$FRANKEN_HOST --port=80 --workers=4 --max-requests=1000 --admin-port=2019 9 | user=root 10 | autostart=true 11 | autorestart=true 12 | priority=2 13 | stdout_events_enabled=true 14 | stderr_events_enabled=true 15 | stdout_logfile=/logs/octane_stdout.log 16 | stderr_logfile=/logs/octane_stderr.log 17 | 18 | 19 | [program:scheduler] 20 | process_name=%(program_name)s_%(process_num)02d 21 | user=root 22 | command=php artisan schedule:work 23 | autostart=true 24 | autorestart=true 25 | numprocs=1 26 | redirect_stderr=true 27 | stdout_logfile=/logs/scheduler.log 28 | 29 | [program:laravel-worker] 30 | process_name=%(program_name)s_%(process_num)02d 31 | user=root 32 | command=php artisan queue:work --max-time=300 33 | autostart=true 34 | autorestart=true 35 | stopasgroup=true 36 | killasgroup=true 37 | numprocs=1 38 | redirect_stderr=true 39 | stdout_logfile=/var/log/worker.log 40 | stopwaitsecs=300 41 | -------------------------------------------------------------------------------- /database/factories/GoldFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class GoldFactory extends Factory 15 | { 16 | /** 17 | * Define the model's default state. 18 | * 19 | * @return array 20 | */ 21 | public function definition(): array 22 | { 23 | return [ 24 | 'name' => $this->faker->name(), 25 | 'weight' => 1, 26 | 'weight_unit' => $this->faker->randomElement(WeightEnum::values()), 27 | 'color' => $this->faker->randomElement(PreciousMetalColorEnum::values()), 28 | 'type' => $this->faker->randomElement(PreciousMetalTypeEnum::values()), 29 | 'karat' => $this->faker->randomElement(KaratEnum::values()), 30 | 'user_id' => $this->faker->numberBetween(1, 1), 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/factories/SilverFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class SilverFactory extends Factory 15 | { 16 | /** 17 | * Define the model's default state. 18 | * 19 | * @return array 20 | */ 21 | public function definition(): array 22 | { 23 | return [ 24 | 25 | 'name' => $this->faker->name(), 26 | 'weight' =>1, 27 | 'weight_unit' => $this->faker->randomElement(WeightEnum::values()), 28 | 'color' => $this->faker->randomElement(PreciousMetalColorEnum::values()), 29 | 'type' => $this->faker->randomElement(PreciousMetalTypeEnum::values()), 30 | 'karat' => $this->faker->randomElement(KaratEnum::values()), 31 | 'user_id' => $this->faker->numberBetween(1, 1), 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'token' => env('POSTMARK_TOKEN'), 19 | ], 20 | 21 | 'ses' => [ 22 | 'key' => env('AWS_ACCESS_KEY_ID'), 23 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 24 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 25 | ], 26 | 27 | 'resend' => [ 28 | 'key' => env('RESEND_KEY'), 29 | ], 30 | 31 | 'slack' => [ 32 | 'notifications' => [ 33 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 34 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 35 | ], 36 | ], 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /database/migrations/2025_01_05_171445_create_money_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->timestamps(); 19 | 20 | $table->string("name", 255); 21 | $table->unsignedInteger("amount")->default(0); 22 | $table->unsignedInteger("usd_amount")->default(0); 23 | $table->string('type'); 24 | $table->text("images")->nullable(); 25 | $table->text("notes")->nullable(); 26 | 27 | 28 | $table->foreignIdFor(User::class); 29 | $table->foreignIdFor(Currency::class)->constrained(); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | */ 36 | public function down(): void 37 | { 38 | Schema::dropIfExists('money'); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call([ 18 | CurrencySeeder::class, 19 | ]); 20 | 21 | // if (app()->isLocal()){ 22 | // 23 | // $this->call([ 24 | // GoldSeeder::class, 25 | // SilverSeeder::class, 26 | // ]); 27 | // 28 | // User::create([ 29 | // 'name' => 'Test', 30 | // 'email' => 'test@test.com', 31 | // 'password' => 'password', 32 | // 'role' => UserRoleEnum::Admin, 33 | // 'currency_id' => 1 34 | // ]); 35 | // } 36 | // 37 | // $this->call([ 38 | // MoneySeeder::class, 39 | // GoldSeeder::class, 40 | // SilverSeeder::class, 41 | // ]); 42 | // 43 | Artisan::call('exchange:refresh-all'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /database/migrations/2025_03_25_064612_create_filament-jobs-monitor_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('job_id')->index(); 17 | $table->string('name')->nullable(); 18 | $table->string('queue')->nullable(); 19 | $table->timestamp('started_at')->nullable()->index(); 20 | $table->timestamp('finished_at')->nullable(); 21 | $table->boolean('failed')->default(false)->index(); 22 | $table->integer('attempt')->default(0); 23 | $table->integer('progress')->nullable(); 24 | $table->text('exception_message')->nullable(); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down(): void 33 | { 34 | Schema::dropIfExists('queue_monitors'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /app/Mail/SendZakahReminderMail.php: -------------------------------------------------------------------------------- 1 | 45 | */ 46 | public function attachments(): array 47 | { 48 | return []; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Console/Commands/GetExchangePriceTodayCommand.php: -------------------------------------------------------------------------------- 1 | json(); 34 | 35 | $rates=$response["conversion_rates"]; 36 | 37 | $rates = Arr::only($rates, Currency::all()->pluck('code')->toArray()); 38 | 39 | 40 | foreach ($rates as $code=>$rate) 41 | \App\Models\Currency::updateOrCreate([ 42 | "code"=>$code 43 | ],[ 44 | "rate" => $rate 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/Mail/WelcomeMail.php: -------------------------------------------------------------------------------- 1 | user->email, 32 | subject: 'Welcome to ' . config('app.name'), 33 | ); 34 | } 35 | 36 | /** 37 | * Get the message content definition. 38 | */ 39 | public function content(): Content 40 | { 41 | return new Content( 42 | view: 'mail.welcome', 43 | ); 44 | } 45 | 46 | /** 47 | * Get the attachments for the message. 48 | * 49 | * @return array 50 | */ 51 | public function attachments(): array 52 | { 53 | return []; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Enums/PreciousMetalTypeEnum.php: -------------------------------------------------------------------------------- 1 | value => self::Bracelet, 26 | self::BodyJewelleries->value => self::BodyJewelleries, 27 | self::Earring->value => self::Earring, 28 | self::Necklace->value => self::Necklace, 29 | self::Ring->value => self::Ring, 30 | ]; 31 | } 32 | public static function non_jewelleries(): array 33 | { 34 | return [ 35 | self::Bar->value => self::Bar, 36 | self::Coin->value => self::Coin, 37 | self::Other->value => self::Other, 38 | ]; 39 | } 40 | 41 | public function getLabel(): ?string 42 | { 43 | return Str::headline($this->name); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Enums/ZakahPaymentMethodEnum.php: -------------------------------------------------------------------------------- 1 | value => self::Cash, 24 | self::CreditCard->value => self::CreditCard, 25 | self::DebitCard->value => self::DebitCard, 26 | self::Crypto->value => self::Crypto, 27 | ]; 28 | } 29 | 30 | 31 | public function getLabel(): ?string 32 | { 33 | return Str::headline($this->name); 34 | } 35 | 36 | public function getColor(): string|array|null 37 | { 38 | return match($this->value) { 39 | self::Cash->value => Color::Green, 40 | self::CreditCard->value => Color::Blue, 41 | self::DebitCard->value => Color::Red, 42 | self::Crypto->value => Color::Gray, 43 | 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /database/migrations/2025_01_05_190204_add_total_zakah_values_to_users_table.php: -------------------------------------------------------------------------------- 1 | string('role')->default('user'); 19 | 20 | // Money Zakah. 21 | $table->unsignedBigInteger('total_money_usd')->default(0); 22 | $table->unsignedBigInteger('total_gold_usd')->default(0); 23 | $table->unsignedBigInteger('total_silver_usd')->default(0); 24 | $table->unsignedBigInteger('total_pay_money')->default(0); 25 | $table->date('next_money_zakah_date')->nullable()->default(null); 26 | 27 | $table->json('settings')->nullable(); 28 | 29 | $table->foreignIdFor(Currency::class)->constrained(); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | */ 36 | public function down(): void 37 | { 38 | Schema::table('users', function (Blueprint $table) { 39 | // 40 | }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /database/migrations/2025_03_25_063525_create_health_tables.php: -------------------------------------------------------------------------------- 1 | getConnectionName(); 14 | $tableName = EloquentHealthResultStore::getHistoryItemInstance()->getTable(); 15 | 16 | Schema::connection($connection)->create($tableName, function (Blueprint $table) { 17 | $table->id(); 18 | 19 | $table->string('check_name'); 20 | $table->string('check_label'); 21 | $table->string('status'); 22 | $table->text('notification_message')->nullable(); 23 | $table->string('short_summary')->nullable(); 24 | $table->json('meta'); 25 | $table->timestamp('ended_at'); 26 | $table->uuid('batch'); 27 | 28 | $table->timestamps(); 29 | }); 30 | 31 | Schema::connection($connection)->table($tableName, function(Blueprint $table) { 32 | $table->index('created_at'); 33 | $table->index('batch'); 34 | }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2025_04_27_185324_create_zakah_payments_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->timestamps(); 19 | 20 | $table->string("name")->nullable(); 21 | $table->unsignedBigInteger("amount"); 22 | $table->unsignedBigInteger("usd_amount"); 23 | $table->string("type"); 24 | $table->string("status"); 25 | $table->string("payment_method")->nullable(); 26 | 27 | $table->date('paid_at')->nullable(); 28 | 29 | $table->text("notes")->nullable(); 30 | $table->text("images")->nullable(); 31 | 32 | $table->foreignIdFor(Currency::class)->nullable()->constrained(); 33 | $table->foreignIdFor(User::class)->constrained(); 34 | }); 35 | } 36 | 37 | /** 38 | * Reverse the migrations. 39 | */ 40 | public function down(): void 41 | { 42 | Schema::dropIfExists('zakah_payments'); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class UserFactory extends Factory 14 | { 15 | /** 16 | * The current password being used by the factory. 17 | */ 18 | protected static ?string $password; 19 | 20 | /** 21 | * Define the model's default state. 22 | * 23 | * @return array 24 | */ 25 | public function definition(): array 26 | { 27 | return [ 28 | 'name' => fake()->name(), 29 | 'email' => fake()->unique()->safeEmail(), 30 | 'email_verified_at' => now(), 31 | 'password' => static::$password ??= Hash::make('password'), 32 | 'remember_token' => Str::random(10), 33 | 'currency_id' => Currency::where('code', 'USD')->first()->id, 34 | 'role' => 'admin', 35 | ]; 36 | } 37 | 38 | /** 39 | * Indicate that the model's email address should be unverified. 40 | */ 41 | public function unverified(): static 42 | { 43 | return $this->state(fn (array $attributes) => [ 44 | 'email_verified_at' => null, 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Enums/PreciousMetalColorEnum.php: -------------------------------------------------------------------------------- 1 | value => Str::headline(self::RoseGold->name), 24 | self::White->value =>Str::headline(self::White->name), 25 | self::Yellow->value => Str::headline(self::Yellow->name), 26 | ]; 27 | } 28 | 29 | public static function silver() 30 | { 31 | return[ 32 | self::Silver->value => Str::headline(self::Silver->name), 33 | ]; 34 | } 35 | 36 | public static function get_badge($value) 37 | { 38 | return match ($value){ 39 | self::Yellow=>Color::Amber, 40 | self::White=>Color::Gray, 41 | self::Silver=>Color::Stone, 42 | self::RoseGold=>Color::hex('#b76e79'), 43 | default=>null, 44 | }; 45 | } 46 | // 47 | public function getLabel(): ?string 48 | { 49 | return Str::headline($this->name); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Observers/UserObserver.php: -------------------------------------------------------------------------------- 1 | updateQuietly([ 19 | "settings" => [ 20 | "consider_jeweleries_in_zakah" => 1, 21 | "enable_top_navbar" => 1 22 | ], 23 | "currency_id" => $user->currency_id ?? Currency::where('code', 'USD')->first()->id, 24 | ]); 25 | 26 | Mail::send(new WelcomeMail($user)); 27 | 28 | } 29 | 30 | /** 31 | * Handle the User "updated" event. 32 | */ 33 | public function updated(User $user): void 34 | { 35 | // 36 | } 37 | 38 | /** 39 | * Handle the User "deleted" event. 40 | */ 41 | public function deleted(User $user): void 42 | { 43 | // 44 | } 45 | 46 | /** 47 | * Handle the User "restored" event. 48 | */ 49 | public function restored(User $user): void 50 | { 51 | // 52 | } 53 | 54 | /** 55 | * Handle the User "force deleted" event. 56 | */ 57 | public function forceDeleted(User $user): void 58 | { 59 | // 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /database/migrations/2025_01_19_154948_create_silvers_table.php: -------------------------------------------------------------------------------- 1 | id(); 20 | $table->timestamps(); 21 | 22 | $table->string('name', 255)->nullable(); 23 | $table->unsignedInteger('weight')->default(0); 24 | $table->unsignedInteger('usd_amount')->default(0); 25 | $table->string('weight_unit')->default(WeightEnum::Gram->value); 26 | $table->unsignedTinyInteger('karat')->default(KaratEnum::Karat24->value); 27 | $table->string('type')->nullable(); 28 | $table->string('color')->nullable(); 29 | $table->text('images')->nullable(); 30 | $table->text('notes')->nullable(); 31 | 32 | $table->foreignIdFor(User::class); 33 | }); 34 | } 35 | 36 | /** 37 | * Reverse the migrations. 38 | */ 39 | public function down(): void 40 | { 41 | Schema::dropIfExists('silvers'); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Zakaty 2 | APP_ENV=production 3 | APP_KEY= 4 | APP_DEBUG=false 5 | APP_TIMEZONE=UTC 6 | APP_URL=http://localhost 7 | 8 | APP_LOCALE=en 9 | APP_FALLBACK_LOCALE=en 10 | APP_FAKER_LOCALE=en_US 11 | 12 | APP_MAINTENANCE_DRIVER=file 13 | # APP_MAINTENANCE_STORE=database 14 | 15 | PHP_CLI_SERVER_WORKERS=4 16 | 17 | BCRYPT_ROUNDS=12 18 | 19 | LOG_CHANNEL=stack 20 | LOG_STACK=single 21 | LOG_DEPRECATIONS_CHANNEL=null 22 | LOG_LEVEL=debug 23 | 24 | DB_CONNECTION=sqlite 25 | # DB_HOST=127.0.0.1 26 | # DB_PORT=3306 27 | # DB_DATABASE=laravel 28 | # DB_USERNAME=root 29 | # DB_PASSWORD= 30 | 31 | SESSION_DRIVER=database 32 | SESSION_LIFETIME=120 33 | SESSION_ENCRYPT=false 34 | SESSION_PATH=/ 35 | SESSION_DOMAIN=null 36 | 37 | BROADCAST_CONNECTION=log 38 | FILESYSTEM_DISK=local 39 | QUEUE_CONNECTION=database 40 | 41 | CACHE_STORE=database 42 | CACHE_PREFIX= 43 | 44 | MEMCACHED_HOST=127.0.0.1 45 | 46 | REDIS_CLIENT=phpredis 47 | REDIS_HOST=127.0.0.1 48 | REDIS_PASSWORD=null 49 | REDIS_PORT=6379 50 | 51 | MAIL_MAILER=log 52 | MAIL_SCHEME=null 53 | MAIL_HOST=127.0.0.1 54 | MAIL_PORT=2525 55 | MAIL_USERNAME=null 56 | MAIL_PASSWORD=null 57 | MAIL_FROM_ADDRESS="hello@example.com" 58 | MAIL_FROM_NAME=Zakaty 59 | 60 | AWS_ACCESS_KEY_ID= 61 | AWS_SECRET_ACCESS_KEY= 62 | AWS_DEFAULT_REGION=us-east-1 63 | AWS_BUCKET= 64 | AWS_USE_PATH_STYLE_ENDPOINT=false 65 | 66 | VITE_APP_NAME="${APP_NAME}" 67 | EXCHANGE_RATE_API_KEY="" 68 | PRECIOUS_METAL_PRICE_API_KEY="" 69 | -------------------------------------------------------------------------------- /app/Models/Money.php: -------------------------------------------------------------------------------- 1 | */ 19 | use HasFactory; 20 | 21 | protected $fillable = [ 22 | 'name', 23 | 'amount', 24 | 'usd_amount', 25 | 'type', 26 | 'images', 27 | 'notes', 28 | 'user_id', 29 | 'currency_id', 30 | ]; 31 | 32 | protected function casts(): array 33 | { 34 | return [ 35 | 'images' => 'array', 36 | 'amount' => \App\Casts\MoneyCast::class, 37 | 'usd_amount' => \App\Casts\MoneyCast::class, 38 | 'type' => MoneyTypeEnum::class, 39 | ]; 40 | } 41 | 42 | 43 | public function currency(): BelongsTo 44 | { 45 | return $this->belongsTo(Currency::class); 46 | } 47 | 48 | public function user(): BelongsTo 49 | { 50 | return $this->belongsTo(User::class); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /database/migrations/2025_01_18_115429_create_gold_table.php: -------------------------------------------------------------------------------- 1 | id(); 20 | $table->timestamps(); 21 | 22 | $table->string('name', 255)->nullable(); 23 | $table->unsignedInteger('weight')->default(0); 24 | $table->unsignedInteger('usd_amount')->default(0); 25 | $table->string('weight_unit')->default(WeightEnum::Gram->value); 26 | $table->unsignedTinyInteger('karat')->default(KaratEnum::Karat24->value); 27 | $table->string('type')->nullable(); 28 | $table->boolean('is_jewellery')->default(true); 29 | $table->string('color')->nullable(); 30 | $table->text('images')->nullable(); 31 | $table->text('notes')->nullable(); 32 | 33 | $table->foreignIdFor(User::class); 34 | }); 35 | 36 | } 37 | 38 | /** 39 | * Reverse the migrations. 40 | */ 41 | public function down(): void 42 | { 43 | Schema::dropIfExists('gold'); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /app/Http/Controllers/ZakahPaymentController.php: -------------------------------------------------------------------------------- 1 | exists($filename)) { 20 | 21 | $query = match ($path) { 22 | 'money' => Money::query(), 23 | 'silver' => Silver::query(), 24 | 'gold' => Gold::query(), 25 | }; 26 | 27 | if ($query->where('user_id', Auth::id())->whereLike('images', "%$filename%")->count()) { 28 | 29 | return Storage::disk($path) 30 | ->response("$filename", $filename, [ 31 | "Content-Type" => "image/png", 32 | ]); 33 | } else { 34 | abort(404); 35 | } 36 | 37 | } 38 | 39 | abort(404); 40 | 41 | 42 | // return response() 43 | // ->file(Storage::disk('dispatch_hub_aws')->temporaryUrl("payment_collections_images/$file_name", now()->addSeconds(5))); 44 | // ->header('Content-Type', 'application/image') 45 | // ->header('Content-Disposition', 'inline'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Models/ZakahPayment.php: -------------------------------------------------------------------------------- 1 | */ 19 | use HasFactory; 20 | 21 | protected $fillable =[ 22 | 'name', 23 | 'amount', 24 | 'usd_amount', 25 | 'type', 26 | 'status', 27 | 'payment_method', 28 | 'paid_at', 29 | 'user_id', 30 | 'currency_id', 31 | 'notes', 32 | 'images', 33 | ]; 34 | 35 | protected function casts(): array 36 | { 37 | return [ 38 | 'amount' => MoneyCast::class, 39 | 'usd_amount' => MoneyCast::class, 40 | 'type' => ZakahPaymentTypeEnum::class, 41 | 'status' => ZakahPaymentStatusEnum::class, 42 | 'payment_method' => ZakahPaymentMethodEnum::class, 43 | 'images' => 'array', 44 | 'paid_at' => 'date', 45 | ]; 46 | } 47 | 48 | public function currency(): BelongsTo 49 | { 50 | return $this->belongsTo(Currency::class); 51 | } 52 | 53 | public function user(): BelongsTo 54 | { 55 | return $this->belongsTo(User::class); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->timestamps(); 22 | }); 23 | 24 | Schema::create('password_reset_tokens', function (Blueprint $table) { 25 | $table->string('email')->primary(); 26 | $table->string('token'); 27 | $table->timestamp('created_at')->nullable(); 28 | }); 29 | 30 | Schema::create('sessions', function (Blueprint $table) { 31 | $table->string('id')->primary(); 32 | $table->foreignId('user_id')->nullable()->index(); 33 | $table->string('ip_address', 45)->nullable(); 34 | $table->text('user_agent')->nullable(); 35 | $table->longText('payload'); 36 | $table->integer('last_activity')->index(); 37 | }); 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | */ 43 | public function down(): void 44 | { 45 | Schema::dropIfExists('users'); 46 | Schema::dropIfExists('password_reset_tokens'); 47 | Schema::dropIfExists('sessions'); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /lang/ar/general.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'today' => "النصاب اليوم", 15 | 'gold' => "سعر الذهب اليوم (1 جرام، 24 قيراط) ", 16 | 'silver' => "سعر الفضة اليوم (1 جرام، 24 قيراط) ", 17 | ], 18 | 19 | 'money_zakah_assets'=>[ 20 | 'title'=> 'أصول زكاة المال', 21 | 'total_money_only' => 'إجمالي المال', 22 | 'total_gold_only' => 'إجمالي الذهب', 23 | 'total_silver_only' => 'إجمالي الفضة', 24 | 'upcoming_title' => 'الزكاة القادمة لجميع الأصول المالية', 25 | 'total_value'=>'القيمة الإجمالية للأصول', 26 | 'next_zakah_date'=>'تاريخ الزكاة القادم', 27 | 'no_zakah_date' => 'لا يوجد زكاة', 28 | 'payment'=>'المبلغ المستحق للدفع', 29 | 'days_for_payment'=>'يوم متبقي حتى الدفع' 30 | ], 31 | 32 | 'reached_with_gold_and_silver' => 'تم الوصول إلى النصاب (مع الذهب والفضة)', 33 | 'remaining_amount_for_nisab' => 'المبلغ المتبقي للوصول إلى النصاب', 34 | 35 | 'money'=>[ 36 | 'title'=>'الأموال', 37 | 'payment'=>'المبلغ المستحق للدفع للحسابات المالية فقط', 38 | 'table'=>[ 39 | 'empty_heading'=> 'لا يوجد مال بعد' 40 | ], 41 | 'type'=>'نوع الحساب', 42 | ], 43 | 44 | 45 | 'gold'=>[ 46 | 'title'=>'الذهب', 47 | 'payment'=>'المبلغ المستحق للدفع للذهب فقط', 48 | 'table'=>[ 49 | 'empty_heading'=> 'لا يوجد ذهب بعد' 50 | ], 51 | 'type'=>'النوع' 52 | ], 53 | 54 | 'currency'=>[ 55 | 'title'=>'العملة', 56 | ] 57 | 58 | 59 | 60 | ]; 61 | -------------------------------------------------------------------------------- /lang/en/general.php: -------------------------------------------------------------------------------- 1 | 'Color', 15 | 'notes' => 'Notes', 16 | 'images' => 'Images', 17 | 18 | 'nisab'=>[ 19 | 'today' => "Nisab Today", 20 | 'gold' => "Gold Price Today (1 gram, 24 karats) ", 21 | 'silver' => "Silver Price Today (1 gram, 24 karats) ", 22 | ], 23 | 24 | 'money_zakah_assets'=>[ 25 | 'title'=> 'Money Zakah Assets', 26 | 'total_money_only' => 'Total Money', 27 | 'total_gold_only' => 'Total Gold', 28 | 'total_silver_only' => 'Total Silver', 29 | 'upcoming_title' => 'Upcoming Zakah For Money Assets', 30 | 'total_value'=>'Total Value Of Assets', 31 | 'next_zakah_date'=>'Next Zakah Date', 32 | 'no_zakah_date'=>'No Zakah', 33 | 'payment'=>'Amount To Pay', 34 | 'days_for_payment'=>'Days Until Payment' 35 | ], 36 | 37 | 'reached_with_gold_and_silver' => 'Reached Nisab (with Gold & Silver)', 38 | 'remaining_amount_for_nisab' => 'Remaining to Reach Nisab', 39 | 'money'=>[ 40 | 'title'=>'Money', 41 | 'payment'=>'Amount To Pay For Money Only', 42 | 'table'=>[ 43 | 'empty_heading'=> 'No Money Yey' 44 | ], 45 | 'type'=>'Type' 46 | ], 47 | 'gold'=>[ 48 | 'title'=>'Gold', 49 | 'payment'=>'Amount To Pay For Gold Only', 50 | 'table'=>[ 51 | 'empty_heading'=> 'No Gold Yet' 52 | ], 53 | 'type'=>'Type' 54 | ], 55 | 56 | 'currency'=>[ 57 | 'title'=>'Currency', 58 | ] 59 | 60 | 61 | ]; 62 | -------------------------------------------------------------------------------- /app/Helpers/CacheHelper.php: -------------------------------------------------------------------------------- 1 | pluck("value", "name"); 15 | }); 16 | } 17 | 18 | public static function get_silver_prices() 19 | { 20 | return Cache::remember("silver_prices", 86400, function () { 21 | return ExchangePrice::where("name", "LIKE", "silver_%")->pluck("value", "name"); 22 | }); 23 | } 24 | 25 | public static function get_precious_metal_price() 26 | { 27 | return Cache::remember("precious_metal_prices", 86400, function () { 28 | return ExchangePrice::whereIn('name', [ 29 | 'gold_price_24', 30 | 'silver_price_24', ]) 31 | ->pluck("value", "name") 32 | ->toArray(); 33 | }); 34 | } 35 | public static function get_nisab() 36 | { 37 | return Cache::remember('today_minimum_nisab', 86400, function () { 38 | // minimum nisab for money is the least between 85g of gold and 595g of silver. 39 | $price_for_gold_24_and_silver_24 = ExchangePrice::whereIn('name', [ 40 | 'gold_price_24', 41 | 'silver_price_24', ]) 42 | ->pluck("value", "name") 43 | ->toArray(); 44 | 45 | return min($price_for_gold_24_and_silver_24['gold_price_24'] * 85, 46 | $price_for_gold_24_and_silver_24['silver_price_24'] * 595); 47 | 48 | }); 49 | } 50 | 51 | 52 | public static function clear_cache_for_zakah_requirements() 53 | { 54 | Cache::forget("today_minimum_nisab"); 55 | Cache::forget("precious_metal_price"); 56 | Cache::forget("silver_prices"); 57 | Cache::forget("gold_prices"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | 24 | Schema::create('job_batches', function (Blueprint $table) { 25 | $table->string('id')->primary(); 26 | $table->string('name'); 27 | $table->integer('total_jobs'); 28 | $table->integer('pending_jobs'); 29 | $table->integer('failed_jobs'); 30 | $table->longText('failed_job_ids'); 31 | $table->mediumText('options')->nullable(); 32 | $table->integer('cancelled_at')->nullable(); 33 | $table->integer('created_at'); 34 | $table->integer('finished_at')->nullable(); 35 | }); 36 | 37 | Schema::create('failed_jobs', function (Blueprint $table) { 38 | $table->id(); 39 | $table->string('uuid')->unique(); 40 | $table->text('connection'); 41 | $table->text('queue'); 42 | $table->longText('payload'); 43 | $table->longText('exception'); 44 | $table->timestamp('failed_at')->useCurrent(); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | */ 51 | public function down(): void 52 | { 53 | Schema::dropIfExists('jobs'); 54 | Schema::dropIfExists('job_batches'); 55 | Schema::dropIfExists('failed_jobs'); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /app/Models/Silver.php: -------------------------------------------------------------------------------- 1 | */ 24 | use HasFactory; 25 | 26 | protected $fillable = [ 27 | 'name', 28 | 'weight', 29 | 'usd_amount', 30 | 'weight_unit', 31 | 'karat', 32 | "type", 33 | 'color', 34 | 'images', 35 | 'notes', 36 | 'user_id', 37 | ]; 38 | 39 | protected function casts(): array 40 | { 41 | return [ 42 | 'images' => 'array', 43 | 'weight' => MoneyCast::class, 44 | 'usd_amount' => MoneyCast::class, 45 | 'weight_unit' => WeightEnum::class, 46 | 'karat' => KaratEnum::class, 47 | 'type' => PreciousMetalTypeEnum::class, 48 | 'color' => PreciousMetalColorEnum::class, 49 | ]; 50 | } 51 | 52 | /** 53 | * Scopes 54 | */ 55 | public function scopeNonJewellery(Builder $query): void 56 | { 57 | $query->whereNotIn('type', PreciousMetalTypeEnum::jewelleries()); 58 | } 59 | 60 | // Attributes 61 | protected function getWeightInGramsAttribute(): float|int 62 | { 63 | return WeightEnum::get_weight_in_gram($this->weight_unit, $this->weight); 64 | } 65 | 66 | // relationships 67 | public function user(): BelongsTo 68 | { 69 | return $this->belongsTo(User::class); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/Filament/Pages/Auth/Register.php: -------------------------------------------------------------------------------- 1 | 0) { 18 | redirect()->to('/login'); 19 | } 20 | 21 | parent::mount(); // TODO: Change the autogenerated stub 22 | } 23 | 24 | public function getRegisterFormAction(): Action 25 | { 26 | if (User::count() > 0) 27 | return Action::make('redirect') 28 | ->action(fn () => redirect()->to('/')); 29 | 30 | return parent::getRegisterFormAction(); 31 | } 32 | 33 | protected function mutateFormDataBeforeRegister(array $data): array 34 | { 35 | $data['role'] = UserRoleEnum::Admin->value; 36 | return parent::mutateFormDataBeforeRegister($data); 37 | } 38 | 39 | protected function getForms(): array 40 | { 41 | $fields= [ 42 | Placeholder::make('disabled') 43 | ]; 44 | 45 | if (User::count() == 0){ 46 | $fields=[ 47 | $this->getNameFormComponent(), 48 | $this->getEmailFormComponent(), 49 | $this->getPasswordFormComponent(), 50 | $this->getPasswordConfirmationFormComponent(), 51 | Select::make('currency_id') 52 | ->label('Default Currency') 53 | ->model(User::class) 54 | ->options(Currency::all()->pluck('code_name', 'id')) 55 | ->native(false) 56 | ->preload() 57 | ->required() 58 | ->searchable(), 59 | ]; 60 | } 61 | 62 | return [ 63 | 'form' => $this->form( 64 | $this->makeForm() 65 | ->schema($fields) 66 | ->statePath('data'), 67 | ), 68 | ]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | filtersLayout(FiltersLayout::AboveContentCollapsible) 39 | ->paginationPageOptions([10, 25, 50, 75, 100, 'all']); 40 | }); 41 | 42 | FilamentView::registerRenderHook( 43 | PanelsRenderHook::SCRIPTS_AFTER, 44 | fn (): string => new HtmlString(''), 45 | ); 46 | 47 | Health::checks([ 48 | OptimizedAppCheck::new(), 49 | DebugModeCheck::new(), 50 | EnvironmentCheck::new(), 51 | ScheduleCheck::new(), 52 | DatabaseCheck::new(), 53 | ]); 54 | 55 | 56 | 57 | Gate::define("view-logs", function (){ 58 | return Auth::user()->role == UserRoleEnum::Admin; 59 | }); 60 | 61 | // LanguageSwitch::configureUsing(function (LanguageSwitch $switch) { 62 | // $switch 63 | // ->locales(['ar','en']); 64 | // }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/Models/Gold.php: -------------------------------------------------------------------------------- 1 | */ 24 | use HasFactory; 25 | 26 | protected $fillable = [ 27 | 'name', 28 | 'weight', 29 | 'usd_amount', 30 | 'weight_unit', 31 | 'karat', 32 | 'type', 33 | 'color', 34 | 'images', 35 | 'notes', 36 | 'user_id', 37 | ]; 38 | 39 | 40 | protected function casts(): array 41 | { 42 | return [ 43 | 'images' => 'array', 44 | 'is_jewellery' => 'boolean', 45 | 'weight' => MoneyCast::class, 46 | 'usd_amount' => MoneyCast::class, 47 | 'weight_unit' => WeightEnum::class, 48 | 'karat' => KaratEnum::class, 49 | 'type' => PreciousMetalTypeEnum::class, 50 | 'color' => PreciousMetalColorEnum::class, 51 | ]; 52 | } 53 | 54 | /** 55 | * Scopes 56 | */ 57 | public function scopeNonJewellery(Builder $query): void 58 | { 59 | $query->where('is_jewellery', false) 60 | ->orWhereNotIn('type', PreciousMetalTypeEnum::jewelleries()); 61 | } 62 | 63 | /** 64 | * Attributes 65 | */ 66 | protected function getWeightInGramsAttribute(): float|int 67 | { 68 | return WeightEnum::get_weight_in_gram($this->weight_unit, $this->weight); 69 | } 70 | 71 | // relationships 72 | public function user(): BelongsTo 73 | { 74 | return $this->belongsTo(User::class); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/Unit/ZakahResetsIfAmountLessThanNisabTest.php: -------------------------------------------------------------------------------- 1 | 'USD', 28 | 'code' => 'USD', 29 | 'rate' => 1, 30 | ], 31 | ]; 32 | 33 | foreach ($currencies as $currency) { 34 | Currency::create($currency); 35 | } 36 | 37 | // set the min nisab 38 | Cache::put('today_minimum_nisab', 50); 39 | 40 | User::factory()->create(); 41 | 42 | } 43 | 44 | public function test_zakah_date_resets_if_value_less_than_nisab(): void 45 | { 46 | $user = User::first(); 47 | 48 | $this->actingAs($user); 49 | 50 | $money_to_create = [ 51 | [ 52 | 'name' => 'First', 53 | 'amount' => 36.7, 54 | 'usd_amount' => 36.7, 55 | 'currency_id' => 1, 56 | 'type' => MoneyTypeEnum::Cash->value, 57 | ], 58 | [ 59 | 'name' => 'Second', 60 | 'amount' => 36.7, 61 | 'usd_amount' => 36.7, 62 | 'currency_id' => 1, 63 | 'type' => MoneyTypeEnum::Cash->value, 64 | ], 65 | ]; 66 | 67 | foreach ($money_to_create as $money) { 68 | $user->money()->create($money); 69 | } 70 | 71 | 72 | $user->refresh(); 73 | 74 | $this->assertNotNull($user->next_money_zakah_date); 75 | 76 | foreach (Money::all() as $money) { 77 | $money->delete(); 78 | } 79 | 80 | $user->refresh(); 81 | 82 | $this->assertNull($user->next_money_zakah_date); 83 | } 84 | 85 | 86 | } 87 | -------------------------------------------------------------------------------- /app/Filament/Widgets/PricesToday.php: -------------------------------------------------------------------------------- 1 | default_currency_code . 27 | Number::format($this->nisab_today_in_default_currency) 28 | )->icon('heroicon-s-banknotes') 29 | ->progress(100) 30 | ->progressBarColor('info') 31 | ->chartColor('info') 32 | ->iconPosition('start') 33 | ->description("$" . Number::format($this->nisab_today) ) 34 | ->iconColor('info'), 35 | 36 | Stat::make(__('general.nisab.gold'),$this->default_currency_code . 37 | Number::format($this->gold_price_in_default_currency) 38 | )->icon('heroicon-s-circle-stack') 39 | ->progress(100) 40 | ->progressBarColor('warning') 41 | ->chartColor('warning') 42 | ->iconPosition('start') 43 | ->description("$" . Number::format($this->gold_price) ) 44 | ->iconColor('warning'), 45 | 46 | Stat::make(__('general.nisab.silver'), 47 | $this->default_currency_code . 48 | Number::format($this->silver_price_in_default_currency) 49 | )->icon('heroicon-s-circle-stack') 50 | ->progress(100) 51 | ->progressBarColor('') 52 | ->chartColor('') 53 | ->iconPosition('start') 54 | ->description("$" . Number::format($this->silver_price)) 55 | ->iconColor(''), 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Filament/Resources/ExchangePriceResource.php: -------------------------------------------------------------------------------- 1 | role == UserRoleEnum::Admin; 29 | } 30 | 31 | public static function form(Form $form): Form 32 | { 33 | return $form 34 | ->schema([ 35 | Forms\Components\TextInput::make('name') 36 | ->required(), 37 | 38 | Forms\Components\TextInput::make('value') 39 | ->required() 40 | ->numeric(), 41 | ]); 42 | } 43 | 44 | public static function table(Table $table): Table 45 | { 46 | return $table 47 | ->columns([ 48 | Tables\Columns\TextColumn::make('name') 49 | ->searchable(), 50 | Tables\Columns\TextColumn::make('value') 51 | ->numeric() 52 | ->sortable(), 53 | 54 | Tables\Columns\TextColumn::make('updated_at') 55 | ->dateTime() 56 | ->sortable(), 57 | ]) 58 | ->actions([ 59 | Tables\Actions\EditAction::make(), 60 | ]); 61 | } 62 | 63 | public static function getRelations(): array 64 | { 65 | return [ 66 | // 67 | ]; 68 | } 69 | 70 | public static function getPages(): array 71 | { 72 | return [ 73 | 'index' => Pages\ListExchangePrices::route('/'), 74 | 'create' => Pages\CreateExchangePrice::route('/create'), 75 | 'edit' => Pages\EditExchangePrice::route('/{record}/edit'), 76 | ]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/Filament/Widgets/MoneyAssetsOverview.php: -------------------------------------------------------------------------------- 1 | default_currency_code . 30 | Number::format($this->total_money_in_default_currency) 31 | )->icon('heroicon-s-banknotes') 32 | ->progress(100) 33 | ->progressBarColor('primary') 34 | ->chartColor('primary') 35 | ->iconPosition('start') 36 | ->description("$" . Number::format($this->total_money)) 37 | ->iconColor('primary'), 38 | 39 | Stat::make(__('general.money_zakah_assets.total_gold_only'), 40 | $this->default_currency_code . 41 | Number::format($this->total_gold_in_default_currency) 42 | ) 43 | ->icon('heroicon-s-circle-stack') 44 | ->progress(100) 45 | ->progressBarColor('warning') 46 | ->chartColor('warning') 47 | ->iconPosition('start') 48 | ->description("$" . Number::format($this->total_gold)) 49 | ->iconColor('warning'), 50 | 51 | Stat::make(__('general.money_zakah_assets.total_silver_only'), 52 | $this->default_currency_code . 53 | Number::format($this->total_silver_in_default_currency) 54 | )->icon('heroicon-s-circle-stack') 55 | ->progress(100) 56 | ->progressBarColor('') 57 | ->chartColor('') 58 | ->iconPosition('start') 59 | ->description("$" . Number::format($this->total_silver)) 60 | ->iconColor(''), 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Filament/Widgets/MoneyAssetsZakahOverview.php: -------------------------------------------------------------------------------- 1 | default_currency_code . 29 | Number::format($this->total_money_assets_value_default_currency) 30 | )->icon('heroicon-s-banknotes') 31 | ->progress(100) 32 | ->progressBarColor('primary') 33 | ->chartColor('primary') 34 | ->iconPosition('start') 35 | ->description("$" . Number::format($this->total_money_assets_value)) 36 | ->iconColor('primary'), 37 | 38 | 39 | Stat::make(__('general.money_zakah_assets.next_zakah_date'), $this->money_assets_zakah_date ?? __('general.money_zakah_assets.no_zakah_date')) 40 | ->icon('heroicon-s-calendar-days') 41 | ->progress(100) 42 | ->progressBarColor('info') 43 | ->chartColor('info') 44 | ->iconPosition('start') 45 | ->description("{$this->days_remaining_to_money_zakah} " . __('general.money_zakah_assets.days_for_payment')) 46 | ->iconColor('info'), 47 | 48 | 49 | Stat::make(__('general.money_zakah_assets.payment'), 50 | $this->default_currency_code . 51 | Number::format($this->money_zakah_to_pay_default_currency) 52 | )->icon('heroicon-s-currency-dollar') 53 | ->progress(100) 54 | ->progressBarColor('danger') 55 | ->chartColor('danger') 56 | ->iconPosition('start') 57 | ->description("$" . Number::format($this->money_zakah_to_pay)) 58 | ->iconColor('danger'), 59 | 60 | 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Console/Commands/GetGoldPriceTodayCommand.php: -------------------------------------------------------------------------------- 1 | get("https://www.goldapi.io/api/XAU/USD")->json(); 35 | 36 | if (Arr::exists($response , "error")){ 37 | Log::error("Something Wrong Happened Getting Gold Prices for date" . today()); 38 | return; 39 | } 40 | 41 | $wanted_settings=[ 42 | [ 43 | "name"=>"gold_price_24", 44 | "api_key"=>"price_gram_24k" 45 | ], 46 | [ 47 | "name"=>"gold_price_22", 48 | "api_key"=>"price_gram_22k" 49 | ], 50 | [ 51 | "name"=>"gold_price_21", 52 | "api_key"=>"price_gram_21k" 53 | ], 54 | [ 55 | "name"=>"gold_price_20", 56 | "api_key"=>"price_gram_20k" 57 | ], 58 | [ 59 | "name"=>"gold_price_18", 60 | "api_key"=>"price_gram_18k" 61 | ], 62 | [ 63 | "name"=>"gold_price_16", 64 | "api_key"=>"price_gram_16k" 65 | ], 66 | [ 67 | "name"=>"gold_price_14", 68 | "api_key"=>"price_gram_14k" 69 | ], 70 | [ 71 | "name"=>"gold_price_10", 72 | "api_key"=>"price_gram_10k" 73 | ], 74 | ]; 75 | 76 | 77 | foreach ($wanted_settings as $setting) 78 | ExchangePrice::updateOrCreate([ 79 | "name" => $setting["name"] 80 | ],[ 81 | "value" => $response[$setting["api_key"]] 82 | ]); 83 | 84 | 85 | Cache::forget("gold_prices"); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/Console/Commands/GetSilverPriceTodayCommand.php: -------------------------------------------------------------------------------- 1 | get("https://www.goldapi.io/api/XAG/USD")->json(); 35 | 36 | if (Arr::exists($response , "error")){ 37 | Log::error("Something Wrong Happened Getting Gold Prices for date" . today()); 38 | return; 39 | } 40 | 41 | $wanted_settings=[ 42 | [ 43 | "name"=>"silver_price_24", 44 | "api_key"=>"price_gram_24k" 45 | ], 46 | [ 47 | "name"=>"silver_price_22", 48 | "api_key"=>"price_gram_22k" 49 | ], 50 | [ 51 | "name"=>"silver_price_21", 52 | "api_key"=>"price_gram_21k" 53 | ], 54 | [ 55 | "name"=>"silver_price_20", 56 | "api_key"=>"price_gram_20k" 57 | ], 58 | [ 59 | "name"=>"silver_price_18", 60 | "api_key"=>"price_gram_18k" 61 | ], 62 | [ 63 | "name"=>"silver_price_16", 64 | "api_key"=>"price_gram_16k" 65 | ], 66 | [ 67 | "name"=>"silver_price_14", 68 | "api_key"=>"price_gram_14k" 69 | ], 70 | [ 71 | "name"=>"silver_price_10", 72 | "api_key"=>"price_gram_10k" 73 | ], 74 | ]; 75 | 76 | 77 | foreach ($wanted_settings as $setting) 78 | ExchangePrice::updateOrCreate([ 79 | "name" => $setting["name"] 80 | ],[ 81 | "value" => $response[$setting["api_key"]] 82 | ]); 83 | 84 | Cache::forget("silver_prices"); 85 | 86 | Log::info("Silver Prices Updated Successfully"); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/Filament/Resources/CurrencyResource.php: -------------------------------------------------------------------------------- 1 | role == UserRoleEnum::Admin; 29 | } 30 | 31 | public static function form(Form $form): Form 32 | { 33 | return $form 34 | ->schema([ 35 | Forms\Components\TextInput::make('name') 36 | ->required() 37 | ->maxLength(255), 38 | 39 | 40 | Forms\Components\TextInput::make('code') 41 | ->required() 42 | ->maxLength(255), 43 | 44 | Forms\Components\TextInput::make('symbol') 45 | ->required() 46 | ->maxLength(255), 47 | 48 | Forms\Components\TextInput::make('rate') 49 | ->label('To USD') 50 | ->numeric() 51 | ->required(), 52 | 53 | 54 | 55 | ]); 56 | } 57 | 58 | public static function table(Table $table): Table 59 | { 60 | return $table 61 | ->columns([ 62 | Tables\Columns\TextColumn::make('name')->searchable()->sortable(), 63 | Tables\Columns\TextColumn::make('code')->searchable()->sortable(), 64 | Tables\Columns\TextColumn::make('symbol')->searchable()->sortable(), 65 | Tables\Columns\TextColumn::make('rate')->label('To USD'), 66 | ]) 67 | ->filters([ 68 | // 69 | ]) 70 | ->actions([ 71 | Tables\Actions\EditAction::make(), 72 | ]) 73 | ->bulkActions([ 74 | Tables\Actions\BulkActionGroup::make([ 75 | Tables\Actions\DeleteBulkAction::make(), 76 | ]), 77 | ]); 78 | } 79 | 80 | public static function getRelations(): array 81 | { 82 | return [ 83 | // 84 | ]; 85 | } 86 | 87 | public static function getPages(): array 88 | { 89 | return [ 90 | 'index' => Pages\ListCurrencies::route('/'), 91 | 'create' => Pages\CreateCurrency::route('/create'), 92 | 'edit' => Pages\EditCurrency::route('/{record}/edit'), 93 | ]; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /public/css/filament/support/support.css: -------------------------------------------------------------------------------- 1 | .fi-pagination-items,.fi-pagination-overview,.fi-pagination-records-per-page-select:not(.fi-compact){display:none}@supports (container-type:inline-size){.fi-pagination{container-type:inline-size}@container (min-width: 28rem){.fi-pagination-records-per-page-select.fi-compact{display:none}.fi-pagination-records-per-page-select:not(.fi-compact){display:inline}}@container (min-width: 56rem){.fi-pagination:not(.fi-simple)>.fi-pagination-previous-btn{display:none}.fi-pagination-overview{display:inline}.fi-pagination:not(.fi-simple)>.fi-pagination-next-btn{display:none}.fi-pagination-items{display:flex}}}@supports not (container-type:inline-size){@media (min-width:640px){.fi-pagination-records-per-page-select.fi-compact{display:none}.fi-pagination-records-per-page-select:not(.fi-compact){display:inline}}@media (min-width:768px){.fi-pagination:not(.fi-simple)>.fi-pagination-previous-btn{display:none}.fi-pagination-overview{display:inline}.fi-pagination:not(.fi-simple)>.fi-pagination-next-btn{display:none}.fi-pagination-items{display:flex}}}.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{background-color:#333;border-radius:4px;color:#fff;font-size:14px;line-height:1.4;outline:0;position:relative;transition-property:transform,visibility,opacity;white-space:normal}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{border-top-color:initial;border-width:8px 8px 0;bottom:-7px;left:0;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:initial;border-width:0 8px 8px;left:0;top:-7px;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-left-color:initial;border-width:8px 0 8px 8px;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{border-right-color:initial;border-width:8px 8px 8px 0;left:-7px;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{color:#333;height:16px;width:16px}.tippy-arrow:before{border-color:transparent;border-style:solid;content:"";position:absolute}.tippy-content{padding:5px 9px;position:relative;z-index:1}.tippy-box[data-theme~=light]{background-color:#fff;box-shadow:0 0 20px 4px #9aa1b126,0 4px 80px -8px #24282f40,0 4px 4px -2px #5b5e6926;color:#26323d}.tippy-box[data-theme~=light][data-placement^=top]>.tippy-arrow:before{border-top-color:#fff}.tippy-box[data-theme~=light][data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:#fff}.tippy-box[data-theme~=light][data-placement^=left]>.tippy-arrow:before{border-left-color:#fff}.tippy-box[data-theme~=light][data-placement^=right]>.tippy-arrow:before{border-right-color:#fff}.tippy-box[data-theme~=light]>.tippy-backdrop{background-color:#fff}.tippy-box[data-theme~=light]>.tippy-svg-arrow{fill:#fff}.fi-sortable-ghost{opacity:.3} -------------------------------------------------------------------------------- /app/Console/Commands/UpdateAllPricesToUsdCommand.php: -------------------------------------------------------------------------------- 1 | toArray(); 39 | 40 | User::with(['silver' => function ($query){ 41 | $query->withoutGlobalScopes(); 42 | }, 43 | 'money'=> function ($query) { 44 | $query->withoutGlobalScopes(); 45 | }, 46 | 'gold'=> function ($query) { 47 | $query->withoutGlobalScopes(); 48 | }, 49 | 'zakah_payments'=> function ($query) { 50 | $query->withoutGlobalScopes(); 51 | }, 52 | 'money.currency']) 53 | ->get() 54 | ->each(function ($user) use ($gold_prices, $silver_prices, $all_currencies) { 55 | 56 | $user->money->each(function ($single_money_account) { 57 | $single_money_account->updateQuietly([ 58 | 'usd_amount' => $single_money_account->amount / $single_money_account->currency->rate 59 | ]); 60 | }); 61 | 62 | 63 | $user->gold->each(function ($gold_account) use ($gold_prices) { 64 | $gold_account->updateQuietly([ 65 | 'usd_amount' => $gold_account->weight_in_grams * $gold_prices['gold_price_' . $gold_account->karat->value] 66 | ]); 67 | }); 68 | 69 | $user->silver->each(function ($silver_account) use ($silver_prices) { 70 | $silver_account->updateQuietly([ 71 | 'usd_amount' => $silver_account->weight_in_grams * $silver_prices['silver_price_' . $silver_account->karat->value] 72 | ]); 73 | }); 74 | 75 | $user->zakah_payments()->each(function (ZakahPayment $zakah_payment) use ($all_currencies) { 76 | $zakah_payment->updateQuietly([ 77 | 'usd_amount' => $zakah_payment->amount / $all_currencies[$zakah_payment->currency_id] 78 | ]); 79 | }); 80 | 81 | 82 | Artisan::call('zakah:refresh',[ 83 | 'user' => $user->id, 84 | ]); 85 | 86 | 87 | }); 88 | 89 | 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /public/js/filament/tables/components/table.js: -------------------------------------------------------------------------------- 1 | function n(){return{checkboxClickController:null,collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,lastCheckedRecord:null,livewireId:null,init:function(){this.livewireId=this.$root.closest("[wire\\:id]").attributes["wire:id"].value,this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1}),this.$nextTick(()=>this.watchForCheckboxClicks()),Livewire.hook("element.init",({component:e})=>{e.id===this.livewireId&&this.watchForCheckboxClicks()})},mountAction:function(e,t=null){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableAction(e,t)},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){if(this.isLoading=!0,this.areRecordsSelected(this.getRecordsInGroupOnPage(e))){this.deselectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e));return}this.selectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e)),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let t=[];for(let s of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])s.dataset.group===e&&t.push(s.value);return t},getRecordsOnPage:function(){let e=[];for(let t of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])e.push(t.value);return e},selectRecords:function(e){for(let t of e)this.isRecordSelected(t)||this.selectedRecords.push(t)},deselectRecords:function(e){for(let t of e){let s=this.selectedRecords.indexOf(t);s!==-1&&this.selectedRecords.splice(s,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(t=>this.isRecordSelected(t))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]},watchForCheckboxClicks:function(){this.checkboxClickController&&this.checkboxClickController.abort(),this.checkboxClickController=new AbortController;let{signal:e}=this.checkboxClickController;this.$root?.addEventListener("click",t=>t.target?.matches(".fi-ta-record-checkbox")&&this.handleCheckboxClick(t,t.target),{signal:e})},handleCheckboxClick:function(e,t){if(!this.lastChecked){this.lastChecked=t;return}if(e.shiftKey){let s=Array.from(this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[]);if(!s.includes(this.lastChecked)){this.lastChecked=t;return}let o=s.indexOf(this.lastChecked),r=s.indexOf(t),l=[o,r].sort((i,d)=>i-d),c=[];for(let i=l[0];i<=l[1];i++)s[i].checked=t.checked,c.push(s[i].value);t.checked?this.selectRecords(c):this.deselectRecords(c)}this.lastChecked=t}}}export{n as default}; 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://getcomposer.org/schema.json", 3 | "name": "laravel/laravel", 4 | "type": "project", 5 | "description": "The skeleton application for the Laravel framework.", 6 | "keywords": ["laravel", "framework"], 7 | "license": "MIT", 8 | "require": { 9 | "php": "^8.3", 10 | "awcodes/filament-quick-create": "^3.6", 11 | "awcodes/palette": "^1.1", 12 | "bezhansalleh/filament-language-switch": "^3.1", 13 | "croustibat/filament-jobs-monitor": "^2.6", 14 | "eightynine/filament-advanced-widgets": "^3.0", 15 | "filament/filament": "^3.3", 16 | "filipfonal/filament-log-manager": "^2.1", 17 | "laravel/framework": "^11.31", 18 | "laravel/octane": "^2.8", 19 | "laravel/tinker": "^2.9", 20 | "leandrocfe/filament-apex-charts": "^3.1", 21 | "malzariey/filament-daterangepicker-filter": "^3.3", 22 | "marvinosswald/filament-input-select-affix": "^0.2.0", 23 | "shuvroroy/filament-spatie-laravel-health": "^2.3" 24 | }, 25 | "require-dev": { 26 | "barryvdh/laravel-debugbar": "^3.15", 27 | "fakerphp/faker": "^1.23", 28 | "laravel/pail": "^1.1", 29 | "laravel/pint": "^1.13", 30 | "laravel/sail": "^1.26", 31 | "mockery/mockery": "^1.6", 32 | "nunomaduro/collision": "^8.1", 33 | "phpunit/phpunit": "^11.0.1" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "App\\": "app/", 38 | "Database\\Factories\\": "database/factories/", 39 | "Database\\Seeders\\": "database/seeders/" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Tests\\": "tests/" 45 | } 46 | }, 47 | "scripts": { 48 | "post-autoload-dump": [ 49 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 50 | "@php artisan package:discover --ansi", 51 | "@php artisan filament:upgrade" 52 | ], 53 | "post-update-cmd": [ 54 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 55 | ], 56 | "post-root-package-install": [ 57 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 58 | ], 59 | "post-create-project-cmd": [ 60 | "@php artisan key:generate --ansi", 61 | "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"", 62 | "@php artisan migrate --graceful --ansi" 63 | ], 64 | "dev": [ 65 | "Composer\\Config::disableProcessTimeout", 66 | "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite" 67 | ] 68 | }, 69 | "extra": { 70 | "laravel": { 71 | "dont-discover": [] 72 | } 73 | }, 74 | "config": { 75 | "optimize-autoloader": true, 76 | "preferred-install": "dist", 77 | "sort-packages": true, 78 | "allow-plugins": { 79 | "pestphp/pest-plugin": true, 80 | "php-http/discovery": true 81 | } 82 | }, 83 | "minimum-stability": "stable", 84 | "prefer-stable": true 85 | } 86 | -------------------------------------------------------------------------------- /tests/Unit/ZakahReminderTest.php: -------------------------------------------------------------------------------- 1 | 'USD', 27 | 'code' => 'USD', 28 | 'rate' => 1, 29 | ], 30 | ]; 31 | 32 | foreach ($currencies as $currency) { 33 | Currency::create($currency); 34 | } 35 | 36 | // set the min nisab 37 | Cache::put('today_minimum_nisab', 50); 38 | 39 | $this->actingAs( User::factory()->create()); 40 | } 41 | 42 | public function test_zakah_reminder_running_before_one_month(): void 43 | { 44 | $user = User::first(); 45 | 46 | $user->update([ 47 | 'next_money_zakah_date' => today()->addMonth(), 48 | ]); 49 | 50 | $money_to_create = [ 51 | [ 52 | 'name' => 'First', 53 | 'amount' => 36700, 54 | 'usd_amount' => 36700, 55 | 'currency_id' => 1, 56 | 'type' => MoneyTypeEnum::Cash->value 57 | ], 58 | [ 59 | 'name' => 'Second', 60 | 'amount' => 36700, 61 | 'usd_amount' => 36700, 62 | 'currency_id' => 1, 63 | 'type' => MoneyTypeEnum::Cash->value 64 | ], 65 | ]; 66 | 67 | foreach ($money_to_create as $money) { 68 | $user->money()->create($money); 69 | } 70 | 71 | 72 | 73 | 74 | Mail::fake(); 75 | 76 | $job = (new SendZakahReminderBeforeThirtyDaysJob)->withFakeQueueInteractions(); 77 | 78 | $job->handle(); 79 | 80 | Mail::assertQueued(SendZakahReminderMail::class); 81 | } 82 | 83 | public function test_zakah_reminder_running_on_payment_day(): void 84 | { 85 | $user = User::first(); 86 | 87 | $user->update([ 88 | 'next_money_zakah_date' => today(), 89 | ]); 90 | 91 | $money_to_create = [ 92 | [ 93 | 'name' => 'First', 94 | 'amount' => 36700, 95 | 'currency_id' => 1, 96 | 'type' => MoneyTypeEnum::Cash->value 97 | ], 98 | [ 99 | 'name' => 'Second', 100 | 'amount' => 367, 101 | 'currency_id' => 1, 102 | 'type' => MoneyTypeEnum::Cash->value 103 | ], 104 | ]; 105 | 106 | foreach ($money_to_create as $money) { 107 | $user->money()->create($money); 108 | } 109 | 110 | Mail::fake(); 111 | 112 | $job = (new SendZakahReminderOnPaymentDay)->withFakeQueueInteractions(); 113 | 114 | $job->handle(); 115 | 116 | Mail::assertQueued(SendZakahReminderMail::class); 117 | } 118 | 119 | 120 | } 121 | -------------------------------------------------------------------------------- /tests/Unit/UserCantAccessOtherUserDataTest.php: -------------------------------------------------------------------------------- 1 | 'USD', 30 | 'code' => 'USD', 31 | 'rate' => 1, 32 | ]); 33 | 34 | ExchangePrice::create([ 35 | 'name' => 'silver_price_24' , 36 | 'value' => 10 37 | ]); 38 | ExchangePrice::create([ 39 | 'name' => 'gold_price_24' , 40 | 'value' => 10 41 | ]); 42 | 43 | User::factory(2)->create(); 44 | 45 | Money::factory(10)->create(); 46 | Gold::factory(10)->create(); 47 | Silver::factory(10)->create(); 48 | ZakahPayment::factory(10)->create(); 49 | 50 | } 51 | 52 | 53 | public function test_user_can_access_his_own_records_only(): void 54 | { 55 | $this->actingAs(User::first()); 56 | 57 | //check user can access all his own records 58 | $gold = Gold::all(); 59 | $silver= Silver::all(); 60 | $zakah_payment= ZakahPayment::all(); 61 | $money= Money::all(); 62 | 63 | 64 | assertEquals(10, $gold->count()); 65 | assertEquals(10, $silver->count()); 66 | assertEquals(10, $zakah_payment->count()); 67 | assertEquals(10, $money->count()); 68 | 69 | 70 | $this->actingAs(User::find(2)); 71 | 72 | $gold = Gold::all(); 73 | $silver= Silver::all(); 74 | $zakah_payment= ZakahPayment::all(); 75 | $money= Money::all(); 76 | 77 | 78 | assertEmpty($gold->count()); 79 | assertEmpty($silver->count()); 80 | assertEmpty($zakah_payment->count()); 81 | assertEmpty( $money->count()); 82 | 83 | 84 | Gold::factory(1)->create(['user_id' => 2]); 85 | Silver::factory(1)->create(['user_id' => 2]); 86 | ZakahPayment::factory(1)->create(['user_id' => 2]); 87 | Money::factory(1)->create(['user_id' => 2]); 88 | 89 | // make sure the second user has records 90 | $gold = Gold::all(); 91 | $silver= Silver::all(); 92 | $zakah_payment= ZakahPayment::all(); 93 | $money= Money::all(); 94 | 95 | assertEquals(1, $gold->count()); 96 | assertEquals(1, $silver->count()); 97 | assertEquals(1, $zakah_payment->count()); 98 | assertEquals(1, $money->count()); 99 | 100 | 101 | //make sure the first user can't access the second user's records 102 | $this->actingAs(User::first()); 103 | $gold = Gold::all(); 104 | $silver= Silver::all(); 105 | $zakah_payment= ZakahPayment::all(); 106 | $money= Money::all(); 107 | 108 | assertEquals(10, $gold->count()); 109 | assertEquals(10, $silver->count()); 110 | assertEquals(10, $zakah_payment->count()); 111 | assertEquals(10, $money->count()); 112 | 113 | 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/Filament/Resources/SilverResource/Widgets/SilverResourceOverview.php: -------------------------------------------------------------------------------- 1 | '$refresh']; 15 | 16 | protected static ?string $pollingInterval='60s'; 17 | 18 | protected function getStats(): array 19 | { 20 | $money_assets_zakah_date =Auth::user()->next_money_zakah_date; 21 | $nisab_today= CacheHelper::get_nisab(); 22 | 23 | $silver_only_value_in_default_currency = Auth::user()->total_silver_usd * Auth::user()->currency->rate; 24 | 25 | $remaining_for_money_to_reach_nisab= $nisab_today - Auth::user()->total_money_assets; 26 | $remaining_for_money_to_reach_nisab_in_default_currency = $remaining_for_money_to_reach_nisab * Auth::user()->currency->rate; 27 | 28 | $amount_to_pay_for_silver_only = ($money_assets_zakah_date) ? Auth::user()->total_silver_usd /40 : 0; 29 | $amount_to_pay_for_silver_only_in_local_currency =$amount_to_pay_for_silver_only * Auth::user()->currency->rate; 30 | 31 | $progress = ($remaining_for_money_to_reach_nisab <= 0) ? 100 : Auth::user()->total_money_assets * 100 / $nisab_today; 32 | $days_remaining_to_zakah = ($money_assets_zakah_date) ? today()->diff($money_assets_zakah_date)->totalDays : 0; 33 | 34 | 35 | return [ 36 | Stat::make('Silver Only Total Value', 37 | Auth::user()->currency->currency_symbol . 38 | Number::format($silver_only_value_in_default_currency) 39 | ) 40 | ->icon('heroicon-s-circle-stack') 41 | ->progress($progress) 42 | ->progressBarColor('primary') 43 | ->chartColor('primary') 44 | ->iconPosition('start') 45 | ->description( 46 | ($money_assets_zakah_date) ? 'Reached Nisab (with Money & Gold) ' : 47 | Auth::user()->currency->currency_symbol . 48 | Number::format($remaining_for_money_to_reach_nisab_in_default_currency) . ' Remaining to Reach Nisab' 49 | ) ->iconColor('primary'), 50 | 51 | Stat::make("Next Zakah Date", $money_assets_zakah_date?->toDateString() ?? 'No Zakah Date') 52 | ->icon('heroicon-s-calendar-days') 53 | ->progress($days_remaining_to_zakah) 54 | ->progressBarColor('info') 55 | ->chartColor('info') 56 | ->iconPosition('start') 57 | ->description("{$days_remaining_to_zakah} Days Until Payment") 58 | ->iconColor('info'), 59 | 60 | 61 | Stat::make("Amount To Pay For Silver Only", 62 | Auth::user()->currency->currency_symbol . 63 | Number::format($amount_to_pay_for_silver_only_in_local_currency) 64 | )->icon('heroicon-s-currency-dollar') 65 | ->progress(100) 66 | ->progressBarColor('danger') 67 | ->chartColor('danger') 68 | ->iconPosition('start') 69 | ->description("$" . Number::format($amount_to_pay_for_silver_only)) 70 | ->iconColor('danger'), 71 | ]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/Filament/Resources/GoldResource/Widgets/GoldResourceOverview.php: -------------------------------------------------------------------------------- 1 | '$refresh']; 16 | 17 | 18 | protected static ?string $pollingInterval='60s'; 19 | 20 | protected function getStats(): array 21 | { 22 | $money_assets_zakah_date =Auth::user()->next_money_zakah_date; 23 | $nisab_today= CacheHelper::get_nisab(); 24 | 25 | $gold_only_value_in_default_currency = Auth::user()->total_gold_usd * Auth::user()->currency->rate; 26 | 27 | $remaining_for_money_to_reach_nisab= $nisab_today - Auth::user()->total_money_assets; 28 | $remaining_for_money_to_reach_nisab_in_default_currency = $remaining_for_money_to_reach_nisab * Auth::user()->currency->rate; 29 | 30 | $amount_to_pay_for_gold_only = ZakahHelper::get_gold_value_that_is_applicable_for_zakah(Auth::user()) / 40 ; 31 | $amount_to_pay_for_gold_only_in_local_currency =$amount_to_pay_for_gold_only * Auth::user()->currency->rate; 32 | 33 | $progress = ($remaining_for_money_to_reach_nisab <= 0) ? 100 : Auth::user()->total_money_assets * 100 / $nisab_today; 34 | $days_remaining_to_zakah = ($money_assets_zakah_date) ? today()->diff($money_assets_zakah_date)->totalDays : 0; 35 | 36 | 37 | return [ 38 | Stat::make('Gold Only Total Value', 39 | Auth::user()->currency->currency_symbol . 40 | Number::format($gold_only_value_in_default_currency) 41 | )->icon('heroicon-s-circle-stack') 42 | ->progress($progress) 43 | ->progressBarColor('primary') 44 | ->chartColor('primary') 45 | ->iconPosition('start') 46 | ->description( 47 | ($money_assets_zakah_date) ? 'Reached Nisab (with Money & Silver) ' : 48 | Auth::user()->currency->currency_symbol . 49 | Number::format($remaining_for_money_to_reach_nisab_in_default_currency) . ' Remaining to Reach Nisab' 50 | ) 51 | ->iconColor('primary'), 52 | 53 | Stat::make("Next Zakah Date", $money_assets_zakah_date?->toDateString() ?? 'No Zakah Date') 54 | ->icon('heroicon-s-calendar-days') 55 | ->progress($days_remaining_to_zakah) 56 | ->progressBarColor('info') 57 | ->chartColor('info') 58 | ->iconPosition('start') 59 | ->description("{$days_remaining_to_zakah} Days Until Payment") 60 | ->iconColor('info'), 61 | 62 | 63 | Stat::make("Amount To Pay For Gold Only", 64 | Auth::user()->currency->currency_symbol . 65 | Number::format($amount_to_pay_for_gold_only_in_local_currency) 66 | )->icon('heroicon-s-currency-dollar') 67 | ->progress(100) 68 | ->progressBarColor('danger') 69 | ->chartColor('danger') 70 | ->iconPosition('start') 71 | ->description("$" . Number::format($amount_to_pay_for_gold_only)) 72 | ->iconColor('danger'), 73 | 74 | 75 | ]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/Filament/Resources/MoneyResource/Widgets/MoneyResourceOverview.php: -------------------------------------------------------------------------------- 1 | '$refresh']; 14 | 15 | protected static ?string $pollingInterval='600s'; 16 | 17 | 18 | protected function getStats(): array 19 | { 20 | $money_assets_zakah_date =Auth::user()->next_money_zakah_date; 21 | $nisab_today= CacheHelper::get_nisab(); 22 | 23 | $money_only_value_in_default_currency = Auth::user()->total_money_usd * Auth::user()->currency->rate; 24 | 25 | $remaining_for_money_to_reach_nisab= $nisab_today - Auth::user()->total_money_assets; 26 | $remaining_for_money_to_reach_nisab_in_default_currency = $remaining_for_money_to_reach_nisab * Auth::user()->currency->rate; 27 | 28 | $amount_to_pay_for_money_only = ($money_assets_zakah_date) ? Auth::user()->total_money_usd /40 : 0; 29 | $amount_to_pay_for_money_only_in_local_currency =$amount_to_pay_for_money_only * Auth::user()->currency->rate; 30 | 31 | $progress = ($remaining_for_money_to_reach_nisab <= 0) ? 100 : Auth::user()->total_money_assets * 100 / $nisab_today; 32 | $days_remaining_to_zakah = ($money_assets_zakah_date) ? today()->diff($money_assets_zakah_date)->totalDays : 0; 33 | 34 | return [ 35 | Stat::make(__('general.money_zakah_assets.total_money_only'), 36 | Auth::user()->currency->currency_symbol . 37 | Number::format($money_only_value_in_default_currency) 38 | )->icon('heroicon-s-banknotes') 39 | ->progress($progress) 40 | ->progressBarColor('primary') 41 | ->chartColor('primary') 42 | ->iconPosition('start') 43 | ->description( 44 | ($money_assets_zakah_date && Auth::user()->total_pay_money) ? " " . __('general.reached_with_gold_and_silver') : 45 | Auth::user()->currency->currency_symbol . 46 | Number::format($remaining_for_money_to_reach_nisab_in_default_currency) . " " . __('general.remaining_amount_for_nisab') 47 | ) 48 | ->iconColor('primary'), 49 | 50 | Stat::make(__('general.money_zakah_assets.next_zakah_date'), $money_assets_zakah_date?->toDateString() ?? __('general.money_zakah_assets.no_zakah_date') ) 51 | ->icon('heroicon-s-calendar-days') 52 | ->progress($days_remaining_to_zakah) 53 | ->progressBarColor('info') 54 | ->chartColor('info') 55 | ->iconPosition('start') 56 | ->description("{$days_remaining_to_zakah} " . __('general.money_zakah_assets.days_for_payment')) 57 | ->iconColor('info'), 58 | 59 | Stat::make(__("general.money.payment"), 60 | Auth::user()->currency->currency_symbol . 61 | Number::format($amount_to_pay_for_money_only_in_local_currency) 62 | )->icon('heroicon-s-currency-dollar') 63 | ->progress(100) 64 | ->progressBarColor('danger') 65 | ->chartColor('danger') 66 | ->iconPosition('start') 67 | ->description("$" . Number::format($amount_to_pay_for_money_only)) 68 | ->iconColor('danger'), 69 | // 70 | 71 | ]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Unit/GoldZakahPaymentTest.php: -------------------------------------------------------------------------------- 1 | 'USD', 28 | 'code' => 'USD', 29 | 'rate' => 1, 30 | ]); 31 | 32 | // set the min nisab 33 | Cache::put('today_minimum_nisab', 100); 34 | 35 | ExchangePrice::create([ 36 | 'name' => 'gold_price_24' , 37 | 'value' => 10 38 | ]); 39 | 40 | $this->actingAs( User::factory()->create()); 41 | 42 | } 43 | 44 | public function test_gold_with_jewellery_considered_payment_amount(): void 45 | { 46 | $golds=[ 47 | [ 48 | 'name'=>Str::random() , 49 | 'weight' => 100, 50 | 'usd_amount' => 1000, 51 | 'weight_unit' => WeightEnum::Gram, 52 | 'karat' => KaratEnum::Karat24, 53 | 'user_id' => 1, 54 | 'type' => PreciousMetalTypeEnum::Ring, 55 | 'is_jewellery' => true, 56 | ], 57 | [ 58 | 'name'=>Str::random() , 59 | 'weight' => 100, 60 | 'usd_amount' => 1000, 61 | 'weight_unit' => WeightEnum::Gram, 62 | 'karat' => KaratEnum::Karat24, 63 | 'user_id' => 1, 64 | 'type' => PreciousMetalTypeEnum::Other, 65 | 'is_jewellery' => false, 66 | ], 67 | ]; 68 | 69 | foreach ($golds as $gold){ 70 | Gold::create($gold); 71 | } 72 | 73 | $gold_value= ZakahHelper::get_gold_value_that_is_applicable_for_zakah(User::first()); 74 | 75 | 76 | $this->assertEquals(2000, $gold_value); 77 | } 78 | 79 | public function test_gold_without_jewellery_considered_payment_amount(): void 80 | { 81 | $golds=[ 82 | [ 83 | 'name'=>Str::random() , 84 | 'weight' => 100, 85 | 'usd_amount' => 1000, 86 | 'weight_unit' => WeightEnum::Gram, 87 | 'karat' => KaratEnum::Karat24, 88 | 'user_id' => 1, 89 | 'type' => PreciousMetalTypeEnum::Ring, 90 | 'is_jewellery' => true, 91 | ], 92 | [ 93 | 'name'=>Str::random() , 94 | 'weight' => 100, 95 | 'usd_amount' => 1000, 96 | 'weight_unit' => WeightEnum::Gram, 97 | 'karat' => KaratEnum::Karat24, 98 | 'user_id' => 1, 99 | 'type' => PreciousMetalTypeEnum::Other, 100 | 'is_jewellery' => false, 101 | ], 102 | ]; 103 | 104 | User::first()->update([ 105 | 'settings' =>[ 106 | 'consider_jeweleries_in_zakah' => false 107 | ] 108 | ]); 109 | 110 | foreach ($golds as $gold){ 111 | Gold::create($gold); 112 | } 113 | 114 | $gold_value= ZakahHelper::get_gold_value_that_is_applicable_for_zakah(User::first()); 115 | 116 | 117 | $this->assertEquals(1000, $gold_value); 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /tests/Unit/SilverZakahPaymentTest.php: -------------------------------------------------------------------------------- 1 | 'USD', 28 | 'code' => 'USD', 29 | 'rate' => 1, 30 | ]); 31 | 32 | // set the min nisab 33 | Cache::put('today_minimum_nisab', 100); 34 | 35 | ExchangePrice::create([ 36 | 'name' => 'silver_price_24' , 37 | 'value' => 10 38 | ]); 39 | 40 | $this->actingAs( User::factory()->create()); 41 | 42 | } 43 | 44 | public function test_silver_with_jewellery_considered_payment_amount(): void 45 | { 46 | $silvers=[ 47 | [ 48 | 'name'=>Str::random() , 49 | 'weight' => 100, 50 | 'usd_amount' => 1000, 51 | 'weight_unit' => WeightEnum::Gram, 52 | 'karat' => KaratEnum::Karat24, 53 | 'user_id' => 1, 54 | 'type' => PreciousMetalTypeEnum::Ring, 55 | 'is_jewellery' => true, 56 | ], 57 | [ 58 | 'name'=>Str::random() , 59 | 'weight' => 100, 60 | 'usd_amount' => 1000, 61 | 'weight_unit' => WeightEnum::Gram, 62 | 'karat' => KaratEnum::Karat24, 63 | 'user_id' => 1, 64 | 'type' => PreciousMetalTypeEnum::Other, 65 | 'is_jewellery' => false, 66 | ], 67 | ]; 68 | 69 | foreach ($silvers as $silver){ 70 | Silver::create($silver); 71 | } 72 | 73 | $silver_value= ZakahHelper::get_silver_value_that_is_applicable_for_zakah(User::first()); 74 | 75 | 76 | $this->assertEquals(2000, $silver_value); 77 | } 78 | 79 | 80 | public function test_silver_without_jewellery_considered_payment_amount(): void 81 | { 82 | $silvers=[ 83 | [ 84 | 'name'=>Str::random() , 85 | 'weight' => 100, 86 | 'usd_amount' => 1000, 87 | 'weight_unit' => WeightEnum::Gram, 88 | 'karat' => KaratEnum::Karat24, 89 | 'user_id' => 1, 90 | 'type' => PreciousMetalTypeEnum::Ring, 91 | 'is_jewellery' => true, 92 | ], 93 | [ 94 | 'name'=>Str::random() , 95 | 'weight' => 100, 96 | 'usd_amount' => 1000, 97 | 'weight_unit' => WeightEnum::Gram, 98 | 'karat' => KaratEnum::Karat24, 99 | 'user_id' => 1, 100 | 'type' => PreciousMetalTypeEnum::Other, 101 | 'is_jewellery' => false, 102 | ], 103 | ]; 104 | 105 | User::first()->update([ 106 | 'settings' =>[ 107 | 'consider_jeweleries_in_zakah' => false 108 | ] 109 | ]); 110 | 111 | foreach ($silvers as $silver){ 112 | Silver::create($silver); 113 | } 114 | 115 | $silver_value= ZakahHelper::get_silver_value_that_is_applicable_for_zakah(User::first()); 116 | 117 | 118 | $this->assertEquals(1000, $silver_value); 119 | } 120 | 121 | 122 | } 123 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Below you may configure as many filesystem disks as necessary, and you 24 | | may even configure multiple disks for the same driver. Examples for 25 | | most supported storage drivers are configured here for reference. 26 | | 27 | | Supported drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app/private'), 36 | 'serve' => true, 37 | 'throw' => false, 38 | ], 39 | 40 | 'public' => [ 41 | 'driver' => 'local', 42 | 'root' => storage_path('app/public'), 43 | 'url' => env('APP_URL').'/storage', 44 | 'visibility' => 'public', 45 | 'throw' => false, 46 | ], 47 | 48 | 'money' => [ 49 | 'driver' => 'local', 50 | 'root' => storage_path('app/public/money'), 51 | 'url' => env('APP_URL').'/storage/money', 52 | 'visibility' => 'public', 53 | 'throw' => false, 54 | ], 55 | 56 | 'zakah_payment' => [ 57 | 'driver' => 'local', 58 | 'root' => storage_path('app/public/zakah_payment'), 59 | 'url' => env('APP_URL').'/storage/zakah_payment', 60 | 'visibility' => 'public', 61 | 'throw' => false, 62 | ], 63 | 64 | 'gold' => [ 65 | 'driver' => 'local', 66 | 'root' => storage_path('app/public/gold'), 67 | 'url' => env('APP_URL').'/storage/gold', 68 | 'visibility' => 'public', 69 | 'throw' => false, 70 | ], 71 | 72 | 'silver' => [ 73 | 'driver' => 'local', 74 | 'root' => storage_path('app/public/silver'), 75 | 'url' => env('APP_URL').'/storage/silver', 76 | 'visibility' => 'public', 77 | 'throw' => false, 78 | ], 79 | 80 | 's3' => [ 81 | 'driver' => 's3', 82 | 'key' => env('AWS_ACCESS_KEY_ID'), 83 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 84 | 'region' => env('AWS_DEFAULT_REGION'), 85 | 'bucket' => env('AWS_BUCKET'), 86 | 'url' => env('AWS_URL'), 87 | 'endpoint' => env('AWS_ENDPOINT'), 88 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 89 | 'throw' => false, 90 | ], 91 | 92 | ], 93 | 94 | /* 95 | |-------------------------------------------------------------------------- 96 | | Symbolic Links 97 | |-------------------------------------------------------------------------- 98 | | 99 | | Here you may configure the symbolic links that will be created when the 100 | | `storage:link` Artisan command is executed. The array keys should be 101 | | the locations of the links and the values should be their targets. 102 | | 103 | */ 104 | 105 | 'links' => [ 106 | public_path('storage') => storage_path('app/public'), 107 | ], 108 | 109 | ]; 110 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_STORE', 'database'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Cache Stores 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the cache "stores" for your application as 26 | | well as their drivers. You may even define multiple stores for the 27 | | same cache driver to group types of items stored in your caches. 28 | | 29 | | Supported drivers: "array", "database", "file", "memcached", 30 | | "redis", "dynamodb", "octane", "null" 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'array' => [ 37 | 'driver' => 'array', 38 | 'serialize' => false, 39 | ], 40 | 41 | 'database' => [ 42 | 'driver' => 'database', 43 | 'connection' => env('DB_CACHE_CONNECTION'), 44 | 'table' => env('DB_CACHE_TABLE', 'cache'), 45 | 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), 46 | 'lock_table' => env('DB_CACHE_LOCK_TABLE'), 47 | ], 48 | 49 | 'file' => [ 50 | 'driver' => 'file', 51 | 'path' => storage_path('framework/cache/data'), 52 | 'lock_path' => storage_path('framework/cache/data'), 53 | ], 54 | 55 | 'memcached' => [ 56 | 'driver' => 'memcached', 57 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 58 | 'sasl' => [ 59 | env('MEMCACHED_USERNAME'), 60 | env('MEMCACHED_PASSWORD'), 61 | ], 62 | 'options' => [ 63 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 64 | ], 65 | 'servers' => [ 66 | [ 67 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 68 | 'port' => env('MEMCACHED_PORT', 11211), 69 | 'weight' => 100, 70 | ], 71 | ], 72 | ], 73 | 74 | 'redis' => [ 75 | 'driver' => 'redis', 76 | 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), 77 | 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), 78 | ], 79 | 80 | 'dynamodb' => [ 81 | 'driver' => 'dynamodb', 82 | 'key' => env('AWS_ACCESS_KEY_ID'), 83 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 84 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 85 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 86 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 87 | ], 88 | 89 | 'octane' => [ 90 | 'driver' => 'octane', 91 | ], 92 | 93 | ], 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Cache Key Prefix 98 | |-------------------------------------------------------------------------- 99 | | 100 | | When utilizing the APC, database, memcached, Redis, and DynamoDB cache 101 | | stores, there might be other applications using the same cache. For 102 | | that reason, you may prefix every cache key to avoid collisions. 103 | | 104 | */ 105 | 106 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), 107 | 108 | ]; 109 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | */ 24 | use HasFactory, Notifiable; 25 | 26 | /** 27 | * The attributes that are mass assignable. 28 | * 29 | * @var list 30 | */ 31 | protected $fillable = [ 32 | 'name', 33 | 'email', 34 | 'password', 35 | 'total_money_usd', 36 | 'next_money_zakah_date', 37 | 'total_pay_money', 38 | 'total_gold_usd', 39 | 'total_silver_usd', 40 | 'settings', 41 | 'currency_id', 42 | 'role', 43 | 'settings.apprise_url', 44 | 'settings.consider_jeweleries_in_zakah', 45 | 'settings.enable_top_navbar', 46 | ]; 47 | 48 | /** 49 | * The attributes that should be hidden for serialization. 50 | * 51 | * @var list 52 | */ 53 | protected $hidden = [ 54 | 'password', 55 | 'remember_token', 56 | 'created_at', 57 | 'updated_at', 58 | ]; 59 | 60 | /** 61 | * Get the attributes that should be cast. 62 | * 63 | * @return array 64 | */ 65 | protected function casts(): array 66 | { 67 | return [ 68 | 'email_verified_at' => 'datetime', 69 | 'password' => 'hashed', 70 | 'settings' => 'json', 71 | 'total_money_usd' => MoneyCast::class, 72 | 'total_gold_usd' => MoneyCast::class, 73 | 'total_silver_usd' => MoneyCast::class, 74 | 'total_pay_money' => MoneyCast::class, 75 | 'next_money_zakah_date' => 'date', 76 | 'role' => UserRoleEnum::class, 77 | 'settings.enable_top_navbar'=>'boolean', 78 | ]; 79 | } 80 | 81 | 82 | /** 83 | * Attributes 84 | */ 85 | 86 | protected function totalMoneyAssets(): Attribute 87 | { 88 | return Attribute::make( 89 | get: fn () => $this->total_money_usd + $this->total_gold_usd + $this->total_silver_usd, 90 | )->shouldCache(); 91 | } 92 | 93 | 94 | 95 | /** 96 | * Relationships 97 | */ 98 | public function currency(): BelongsTo 99 | { 100 | return $this->belongsTo(Currency::class); 101 | } 102 | 103 | public function family(): BelongsTo 104 | { 105 | return $this->belongsTo(Family::class); 106 | } 107 | 108 | public function parent(): BelongsTo 109 | { 110 | return $this->belongsTo(User::class); 111 | } 112 | 113 | public function money(): HasMany 114 | { 115 | return $this->hasMany(Money::class); 116 | } 117 | 118 | public function gold(): HasMany 119 | { 120 | return $this->hasMany(Gold::class); 121 | } 122 | 123 | public function silver(): HasMany 124 | { 125 | return $this->hasMany(Silver::class); 126 | } 127 | 128 | public function zakah_payments(): User|HasMany 129 | { 130 | return $this->hasMany(ZakahPayment::class); 131 | } 132 | 133 | public function canAccessPanel(Panel $panel): bool 134 | { 135 | return true; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_MAILER', 'log'), 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Mailer Configurations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Here you may configure all of the mailers used by your application plus 25 | | their respective settings. Several examples have been configured for 26 | | you and you are free to add your own as your application requires. 27 | | 28 | | Laravel supports a variety of mail "transport" drivers that can be used 29 | | when delivering an email. You may specify which one you're using for 30 | | your mailers below. You may also add additional mailers if needed. 31 | | 32 | | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", 33 | | "postmark", "resend", "log", "array", 34 | | "failover", "roundrobin" 35 | | 36 | */ 37 | 38 | 'mailers' => [ 39 | 40 | 'smtp' => [ 41 | 'transport' => 'smtp', 42 | 'scheme' => env('MAIL_SCHEME'), 43 | 'url' => env('MAIL_URL'), 44 | 'host' => env('MAIL_HOST', '127.0.0.1'), 45 | 'port' => env('MAIL_PORT', 2525), 46 | 'username' => env('MAIL_USERNAME'), 47 | 'password' => env('MAIL_PASSWORD'), 48 | 'timeout' => null, 49 | 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), 50 | ], 51 | 52 | 'ses' => [ 53 | 'transport' => 'ses', 54 | ], 55 | 56 | 'postmark' => [ 57 | 'transport' => 'postmark', 58 | // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), 59 | // 'client' => [ 60 | // 'timeout' => 5, 61 | // ], 62 | ], 63 | 64 | 'resend' => [ 65 | 'transport' => 'resend', 66 | ], 67 | 68 | 'sendmail' => [ 69 | 'transport' => 'sendmail', 70 | 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), 71 | ], 72 | 73 | 'log' => [ 74 | 'transport' => 'log', 75 | 'channel' => env('MAIL_LOG_CHANNEL'), 76 | ], 77 | 78 | 'array' => [ 79 | 'transport' => 'array', 80 | ], 81 | 82 | 'failover' => [ 83 | 'transport' => 'failover', 84 | 'mailers' => [ 85 | 'smtp', 86 | 'log', 87 | ], 88 | ], 89 | 90 | 'roundrobin' => [ 91 | 'transport' => 'roundrobin', 92 | 'mailers' => [ 93 | 'ses', 94 | 'postmark', 95 | ], 96 | ], 97 | 98 | ], 99 | 100 | /* 101 | |-------------------------------------------------------------------------- 102 | | Global "From" Address 103 | |-------------------------------------------------------------------------- 104 | | 105 | | You may wish for all emails sent by your application to be sent from 106 | | the same address. Here you may specify a name and address that is 107 | | used globally for all emails that are sent by your application. 108 | | 109 | */ 110 | 111 | 'from' => [ 112 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 113 | 'name' => env('MAIL_FROM_NAME', 'Example'), 114 | ], 115 | 116 | ]; 117 | -------------------------------------------------------------------------------- /app/Providers/Filament/AdminPanelProvider.php: -------------------------------------------------------------------------------- 1 | default() 40 | ->id('admin') 41 | ->path('/') 42 | ->login() 43 | ->registration(Register::class) 44 | ->login(action: \App\Filament\Pages\Auth\Login::class) 45 | ->profile(EditProfile::class,false) 46 | ->colors (fn ()=> [ 47 | 'primary' => match(true){ 48 | Str::startsWith( Request::getPathInfo(), '/gold')=> Color::Amber, 49 | Str::startsWith( Request::getPathInfo(), '/silver')=> Color::Stone, 50 | default => Color::Green 51 | } 52 | ]) 53 | ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') 54 | ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') 55 | ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') 56 | ->middleware([ 57 | EncryptCookies::class, 58 | AddQueuedCookiesToResponse::class, 59 | StartSession::class, 60 | AuthenticateSession::class, 61 | ShareErrorsFromSession::class, 62 | VerifyCsrfToken::class, 63 | SubstituteBindings::class, 64 | DisableBladeIconComponents::class, 65 | DispatchServingFilamentEvent::class, 66 | ]) 67 | ->authMiddleware([ 68 | Authenticate::class, 69 | ]) 70 | ->plugins([ 71 | FilamentSpatieLaravelHealthPlugin::make() 72 | ->authorize(fn () => Auth::user()->role == UserRoleEnum::Admin), 73 | FilamentLogManager::make(), 74 | QuickCreatePlugin::make() 75 | ->excludes([ 76 | UserResource::class 77 | ]) 78 | ->sortBy('navigation'), 79 | FilamentJobsMonitorPlugin::make() 80 | ->enableNavigation(fn () => Auth::user()->role == UserRoleEnum::Admin), 81 | FilamentApexChartsPlugin::make() 82 | 83 | ]) 84 | ->passwordReset() 85 | // ->brandLogo(logo: asset("storage/bandit.png")) 86 | ->brandName(config('app.name')) 87 | ->maxContentWidth(MaxWidth::Full) 88 | ->unsavedChangesAlerts() 89 | ->databaseTransactions() 90 | ->breadcrumbs(false) 91 | ->sidebarFullyCollapsibleOnDesktop() 92 | ->topNavigation(fn()=> Auth::user()->settings['enable_top_navbar']) 93 | ; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'database'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection options for every queue backend 24 | | used by your application. An example configuration is provided for 25 | | each backend supported by Laravel. You're also free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'connection' => env('DB_QUEUE_CONNECTION'), 40 | 'table' => env('DB_QUEUE_TABLE', 'jobs'), 41 | 'queue' => env('DB_QUEUE', 'default'), 42 | 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), 43 | 'after_commit' => false, 44 | ], 45 | 46 | 'beanstalkd' => [ 47 | 'driver' => 'beanstalkd', 48 | 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), 49 | 'queue' => env('BEANSTALKD_QUEUE', 'default'), 50 | 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), 51 | 'block_for' => 0, 52 | 'after_commit' => false, 53 | ], 54 | 55 | 'sqs' => [ 56 | 'driver' => 'sqs', 57 | 'key' => env('AWS_ACCESS_KEY_ID'), 58 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 59 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 60 | 'queue' => env('SQS_QUEUE', 'default'), 61 | 'suffix' => env('SQS_SUFFIX'), 62 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 63 | 'after_commit' => false, 64 | ], 65 | 66 | 'redis' => [ 67 | 'driver' => 'redis', 68 | 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 69 | 'queue' => env('REDIS_QUEUE', 'default'), 70 | 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), 71 | 'block_for' => null, 72 | 'after_commit' => false, 73 | ], 74 | 75 | ], 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Job Batching 80 | |-------------------------------------------------------------------------- 81 | | 82 | | The following options configure the database and table that store job 83 | | batching information. These options can be updated to any database 84 | | connection and table which has been defined by your application. 85 | | 86 | */ 87 | 88 | 'batching' => [ 89 | 'database' => env('DB_CONNECTION', 'sqlite'), 90 | 'table' => 'job_batches', 91 | ], 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Failed Queue Jobs 96 | |-------------------------------------------------------------------------- 97 | | 98 | | These options configure the behavior of failed queue job logging so you 99 | | can control how and where failed jobs are stored. Laravel ships with 100 | | support for storing failed jobs in a simple file or in a database. 101 | | 102 | | Supported drivers: "database-uuids", "dynamodb", "file", "null" 103 | | 104 | */ 105 | 106 | 'failed' => [ 107 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 108 | 'database' => env('DB_CONNECTION', 'sqlite'), 109 | 'table' => 'failed_jobs', 110 | ], 111 | 112 | ]; 113 | --------------------------------------------------------------------------------