├── 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 |
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 | 
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 |
--------------------------------------------------------------------------------