mohamedsabil83 / filament-hijri-picker

A Hijri datetime picker component for Filament
MIT License
22 stars 5 forks source link

[Bug]: Date Selection in HijriDatePicker not working as expected #22

Open CodeDanger opened 2 months ago

CodeDanger commented 2 months ago

What happened?

so whenever i try to set hijri date picker to live mode and give it some after state updated there is 2 problem appears first one is assume today one of these days [21,22,23,24,25] some days depend in month day count 30 or 29 if you hover with mouse over todays date then go again to the end date hover it the current month will be automaticly switched to the next month i have attached video for this process so you can understand

https://github.com/user-attachments/assets/4ba3d050-3909-47a5-97d4-88d95dc9e56e

the second point is when ever try to select last day in current month it doesn't selected instead it select the next day which is the first date in the next month

here is specific part of code causes error ` // Hijri Date

                    Section::make([
                        HijriDatePicker::make('start_date_hijri')
                            ->label(__('تاريخ بدء العقد'))
                            ->required()
                            ->columnSpan(1)
                            ->format('Y-m-d')
                            ->displayFormat('Y-m-d')
                            ->native(false)
                            ->default(toHijriDatePicker(null))
                            ->closeOnDateSelection()
                            ->afterStateUpdated(fn ($state, callable $set)=> $set('start_date',toGregoryDatePicker($state)))
                            ->live(onBlur:true),
                        HijriDatePicker::make('end_date_hijri')
                            ->label(__('تاريخ انتهاء العقد'))
                            ->required()
                            ->native(false)
                            ->columnSpan(1)
                            ->format('Y-m-d')
                            ->displayFormat('Y-m-d')
                            ->default(toHijri()->addYear())
                            ->closeOnDateSelection()
                            ->afterStateUpdated(fn ($state, callable $set)=> $set('end_date',toGregoryDatePicker($state)))
                            ->live(onBlur:true),
                    ])
                    ->heading(__('التاريخ الهجري'))
                    ->columnSpan(1)
                    ->columns(2),

                    // Gorgeian Date
                    Section::make([
                        DatePicker::make('start_date')
                            ->label(__('تاريخ بدء العقد'))
                            // day-month-year
                            // ->format('D-m-Y')
                            ->required()
                            ->columnSpan(1)
                            ->native(false)
                            ->displayFormat('Y-m-d')
                            ->format('Y-m-d')
                            ->closeOnDateSelection()
                            ->afterStateUpdated(fn ($state, callable $set)=> $set('start_date_hijri',toHijriDatePicker($state)))
                            ->afterStateHydrated(fn (callable $set,callable $get,?string $context)=>$context!=='create'?  $set('start_date_hijri',toHijriDatePicker($get('start_date'))):null)
                            ->default(fn(callable $get)=>toGregoryDatePicker($get('start_date_hijri')))
                            ->live(onBlur:true),
                        DatePicker::make('end_date')
                            ->label(__('تاريخ انتهاء العقد'))
                            ->required()
                            ->columnSpan(1)
                            ->native(false)
                            ->displayFormat('Y-m-d')
                            ->format('Y-m-d')
                            ->closeOnDateSelection()
                            ->afterStateUpdated(fn ($state, callable $set)=> $set('end_date_hijri',toHijriDatePicker($state)))
                            ->afterStateHydrated(fn (callable $set,callable $get,?string $context)=>$context!=='create'?   $set('end_date_hijri',toHijriDatePicker($get('end_date'))):null)
                            ->default(fn(callable $get)=>toGregoryDatePicker($get('end_date_hijri')))
                            ->live(onBlur:true),
                    ])
                    ->live(onBlur:true)
                    ->afterStateUpdated(fn(?string $context,callable $set,callable $get)=>ContractResource::updatePayments($context, $set, $get))
                    ->heading(__('التاريخ الميلادي'))
                    ->columnSpan(1)
                    ->columns(2),

`

and here is the full resource code

`<?php

namespace App\Filament\Resources;

use App\Filament\Actions\Tables\ExportPdfAction; use App\Filament\Exports\ContractExporter; use App\Models\Contract; use Filament\Resources\Resource; use App\Filament\Resources\ContractResource\Pages; use Filament\Forms\Form; use Filament\Forms\Components{Actions, Component, Select, DatePicker, TextInput, Textarea, Repeater, Section, Tabs}; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Actions\EditAction; use Filament\Tables\Actions\DeleteAction; use Filament\Tables\Table; use Illuminate\Validation\Rule; use Filament\Resources\Pages\CreateRecord; use Filament\Tables\Actions\Action; use Filament\Tables\Actions\ActionGroup; use MohamedSabil83\FilamentHijriPicker\Forms\Components\HijriDatePicker; use Filament\Support\Colors\Color; use Carbon\Carbon; use Carbon\CarbonInterval;

class ContractResource extends Resource { protected static ?string $model = Contract::class; protected static ?string $navigationIcon = 'heroicon-o-document-text'; protected static ?int $navigationSort = 4;

public static function getNavigationGroup(): string
{
    return __('إدارة المستاجرين');
}

public static function getNavigationLabel(): string
{
    return __('إدارة العقود');
}

public static function getPluralLabel(): string
{
    return __('العقود');
}

public static function getLabel(): string
{
    return __('عقد');
}

public static function calculateRequiredPayments($start_date, $end_date, $payment_cycle) {
    // Convert dates to Carbon instances
    $start =  Carbon::parse($start_date);
    $end = Carbon::parse($end_date);

    // Define intervals for each payment cycle
    $intervals = [
        'prepaid' => CarbonInterval::days(1), // Prepaid is considered as a single payment
        'daily' => CarbonInterval::days(1), // Daily payments
        'monthly' => CarbonInterval::months(1), // Monthly payments
        'quarterly' => CarbonInterval::months(3), // Quarterly payments
        'semi_annual' => CarbonInterval::months(6), // Semi-annual payments
        'annual' => CarbonInterval::years(1) // Annual payments
    ];

    if (!array_key_exists($payment_cycle, $intervals)) {
        return false;
    }

    $interval = $intervals[$payment_cycle];

    if ($payment_cycle === 'prepaid') {
        return 1;
    }

    $count = 0;
    $current = $start->copy();

    while ($current <= $end) {
        $count++;
        $current->add($interval);
    }

    return ceil($count);
}

public static function generatePaymentsArray($start_date, $end_date, $payment_cycle,$rent) {
    // Convert dates to Carbon instances
    $start = Carbon::parse($start_date);
    $end = Carbon::parse($end_date);

    // Define intervals for each payment cycle
    $intervals = [
        'prepaid' => CarbonInterval::days(1), // Prepaid is considered as a single payment
        'daily' => CarbonInterval::days(1), // Daily payments
        'monthly' => CarbonInterval::months(1), // Monthly payments
        'quarterly' => CarbonInterval::months(3), // Quarterly payments
        'semi_annual' => CarbonInterval::months(6), // Semi-annual payments
        'annual' => CarbonInterval::years(1) // Annual payments
    ];

    if (!array_key_exists($payment_cycle, $intervals)) {
        return false;
    }

    $interval = $intervals[$payment_cycle];

    if ($payment_cycle === 'prepaid') {
        // For prepaid, return a single payment
        return [
            [
                'date' => $start->toDateString(),
                'date_hijri' => toHijriDatePicker($start->toDateString()),
                // 'pay_date' => $start->toDateString(),
                // 'pay_date_hijri' => toHijriDatePicker($start->toDateString()),
                'amount' => $rent, // Default amount
                'paid_amount' => 0, // Default paid amount
                'status' => 'pending', // Default status
                'pay_type' => null, // Default payment type
                'details' => null // Default details
            ]
        ];
    }

    // Calculate the number of required payments
    $count = 0;
    $current = $start->copy();

    $payments = [];

    while ($current->lessThanOrEqualTo($end)) {
        $payments[] = [
            'date' => $current->toDateString(),
            'date_hijri' => toHijriDatePicker($current->toDateString()),
            // 'pay_date' => $current->toDateString(),
            // 'pay_date_hijri' => toHijriDatePicker($current->toDateString()),
            'amount' => (int)$rent, // Default amount
            'paid_amount' => 0, // Default paid amount
            'status' => 'pending', // Default status
            'pay_type' => null, // Default payment type
            'details' => null // Default details
        ];

        $current->add($interval);
    }

    return $payments;
}
public static function updatePayments($context,$set, $get)
{
    if(!$context==='create')return;
    $conditions_arr=[
        $get('start_date')!==null,
        $get('end_date')!==null,
        $get('end_date')!==null,
        $get('rent')!==null,
    ];
    if(in_array(false,$conditions_arr,true))return;
    // $payments = ContractResource::calculateRequiredPayments(
    $payments = ContractResource::generatePaymentsArray(
        $get('start_date'),
        $get('end_date'),
        $get('payment_cycle'),
        $get('rent')
    );
    // if date difference = 0 or payment cycle is not exist
    if(!$payments) return;
    $set('payments',$payments);

}

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Tabs::make(__('العقد'))
                ->tabs([
                    Tabs\Tab::make(__('المعلومات الاساسية'))
                    ->schema([
                        Select::make('unit_id')
                        ->label(__('الوحدة'))
                        ->relationship('unit', 'name', function ($query) {
                            $query->free();
                        })
                        ->visibleOn(['create'])
                        ->searchable()
                        ->preload()
                        ->columnSpan(1)
                        ->required(),
                    Select::make('unit_id')
                        ->label(__('الوحدة'))
                        ->relationship('unit', 'name')
                        ->visibleOn(['edit','show'])
                        ->searchable()
                        ->preload()
                        ->columnSpan(1)
                        ->required(),
                    Select::make('tenant_id')
                        ->label(__('المستأجر'))
                        ->relationship('tenant', 'name')
                        // here search with id also 1181815315
                        ->searchable(['name','national_id'])
                        ->preload()
                        ->columnSpan(1)
                        ->required(),

                    // Hijri Date

                    Section::make([
                        HijriDatePicker::make('start_date_hijri')
                            ->label(__('تاريخ بدء العقد'))
                            ->required()
                            ->columnSpan(1)
                            ->format('Y-m-d')
                            ->displayFormat('Y-m-d')
                            ->native(false)
                            ->default(toHijriDatePicker(null))
                            ->closeOnDateSelection()
                            ->afterStateUpdated(fn ($state, callable $set)=> $set('start_date',toGregoryDatePicker($state)))
                            ->live(onBlur:true),
                        HijriDatePicker::make('end_date_hijri')
                            ->label(__('تاريخ انتهاء العقد'))
                            ->required()
                            ->native(false)
                            ->columnSpan(1)
                            ->format('Y-m-d')
                            ->displayFormat('Y-m-d')
                            ->default(toHijri()->addYear())
                            ->closeOnDateSelection()
                            ->afterStateUpdated(fn ($state, callable $set)=> $set('end_date',toGregoryDatePicker($state)))
                            ->live(onBlur:true),
                    ])
                    ->heading(__('التاريخ الهجري'))
                    ->columnSpan(1)
                    ->columns(2),

                    // Gorgeian Date
                    Section::make([
                        DatePicker::make('start_date')
                            ->label(__('تاريخ بدء العقد'))
                            // day-month-year
                            // ->format('D-m-Y')
                            ->required()
                            ->columnSpan(1)
                            ->native(false)
                            ->displayFormat('Y-m-d')
                            ->format('Y-m-d')
                            ->closeOnDateSelection()
                            ->afterStateUpdated(fn ($state, callable $set)=> $set('start_date_hijri',toHijriDatePicker($state)))
                            ->afterStateHydrated(fn (callable $set,callable $get,?string $context)=>$context!=='create'?  $set('start_date_hijri',toHijriDatePicker($get('start_date'))):null)
                            ->default(fn(callable $get)=>toGregoryDatePicker($get('start_date_hijri')))
                            ->live(onBlur:true),
                        DatePicker::make('end_date')
                            ->label(__('تاريخ انتهاء العقد'))
                            ->required()
                            ->columnSpan(1)
                            ->native(false)
                            ->displayFormat('Y-m-d')
                            ->format('Y-m-d')
                            ->closeOnDateSelection()
                            ->afterStateUpdated(fn ($state, callable $set)=> $set('end_date_hijri',toHijriDatePicker($state)))
                            ->afterStateHydrated(fn (callable $set,callable $get,?string $context)=>$context!=='create'?   $set('end_date_hijri',toHijriDatePicker($get('end_date'))):null)
                            ->default(fn(callable $get)=>toGregoryDatePicker($get('end_date_hijri')))
                            ->live(onBlur:true),
                    ])
                    ->live(onBlur:true)
                    ->afterStateUpdated(fn(?string $context,callable $set,callable $get)=>ContractResource::updatePayments($context, $set, $get))
                    ->heading(__('التاريخ الميلادي'))
                    ->columnSpan(1)
                    ->columns(2),

                    TextInput::make('rent')
                        ->label(__('الإيجار'))
                        ->numeric()
                        ->columnSpan(1)
                        ->afterStateUpdated(fn(?string $context,callable $set,callable $get)=>ContractResource::updatePayments($context, $set, $get))
                        ->required()
                        ->live(debounce:500),
                    Select::make('payment_cycle')
                        ->label(__('دورية الدفع'))
                        ->required()
                        ->options([
                            'prepaid' => __('مدفوع مسبقا'),
                            'daily' => __('يومي'),
                            'monthly' => __('شهري'),
                            'quarterly' => __('ربع سنوي'),
                            'semi_annual' => __('نصف سنوي'),
                            'annual' => __('سنوي'),
                        ])
                        ->searchable()
                        ->default('monthly')
                        ->afterStateUpdated(fn(?string $context,callable $set,callable $get)=>ContractResource::updatePayments($context, $set, $get))
                        ->live()
                        ->columnSpan(1),
                    Textarea::make('details')
                        ->label(__('تفاصيل العقد')),

                    ]),

                // payments tab
                Tabs\Tab::make(__('ادارة الدفعات'))
                ->schema([
                    Repeater::make('payments')
                    ->label(__('دفعات الإيجار'))
                    ->collapsed()
                    // ->relationship('payments')
                    ->grid(2)
                    ->itemLabel(fn (array $state): ?string => $state['date'].' / '.$state['date_hijri'] ?? null)
                    ->cloneable()
                    ->schema([
                        Section::make([
                            DatePicker::make('date')
                                ->label(__('تاريخ الدفع الميلادي'))
                                ->required()
                                ->reactive()
                                ->native(false)
                                ->displayFormat('Y-m-d')
                                ->format('Y-m-d')
                                ->afterStateUpdated(fn (callable $set, $state) => $set('date_hijri', toHijriDatePicker($state))),

                            HijriDatePicker::make('date_hijri')
                                ->label(__('تاريخ الدفع الهجري'))
                                ->required()
                                ->format('Y-m-d')
                                ->displayFormat('Y-m-d')
                                ->reactive()
                                ->afterStateUpdated(fn (callable $set, $state) => $set('date', toGregoryDatePicker($state))),

                            DatePicker::make('pay_date')
                                ->label(__('تاريخ الدفع الفعلي الميلادي'))
                                ->nullable()
                                ->native(false)
                                ->displayFormat('Y-m-d')
                                ->format('Y-m-d')
                                ->reactive()
                                ->afterStateUpdated(fn (callable $set, $state) => $set('pay_date_hijri', toHijriDatePicker($state))),

                            HijriDatePicker::make('pay_date_hijri')
                                ->label(__('تاريخ الدفع الفعلي الهجري'))
                                ->nullable()
                                ->format('Y-m-d')
                                ->displayFormat('Y-m-d')
                                ->reactive()
                                ->afterStateUpdated(fn (callable $set, $state) => $set('pay_date', toGregoryDatePicker($state))),
                        ])
                        ->heading(__('التاريخ'))
                        ->columnSpan(2)
                        ->columns(2),

                        TextInput::make('amount')
                            ->label(__('المبلغ'))
                            ->numeric()
                            ->required(),

                        TextInput::make('paid_amount')
                            ->label(__('المبلغ المدفوع'))
                            ->numeric()
                            ->default(0)
                            ->required(),

                        Select::make('status')
                            ->label(__('الحالة'))
                            ->options([
                                'pending' => __('معلقة'),
                                'paid' => __('مدفوعة'),
                            ])
                            ->native(false)
                            ->default('pending')
                            ->columnSpan(1),

                        Select::make('pay_type')
                            ->label(__('نوع الدفع'))
                            ->native(false)
                            ->options([
                                'cash' => __('نقدي'),
                                'credit' => __('بطاقة ائتمان'),
                                'bank-transfare' => __('تحويل بنكي'),
                            ])
                            ->nullable()
                            ->columnSpan(1),

                        Textarea::make('details')
                            ->label(__('التفاصيل'))
                            ->nullable()
                            ->columnSpan(1),
                    ])
                    ->defaultItems(0)
                    ->disabled(fn (callable $get) => !$get('rent') || !$get('start_date') || !$get('end_date') || !$get('payment_cycle'))
                    // ->live()
                    ->reorderable(false)
                    ->columns(2)
                    ->columnSpan(2),

                ]),

                ])
                ->columns(2)
                ->columnSpan('full'),
        ]);
}

public static function table(Table $table): Table
{
    return $table
        ->columns([
            // TextColumn::make('contract_number')
            //     ->label(__('رقم العقد'))
            //     ->sortable()
            //     ->searchable(),
            TextColumn::make('unit.name') // Display unit name
                ->label(__('الوحدة'))
                ->searchable(),
            TextColumn::make('tenant.name') // Display tenant name
                ->label(__('المستأجر'))
                ->searchable(),
            TextColumn::make('tenant.national_id') // Display tenant name
                ->label(__('رقم الهوية / الاقامة / الحدود'))
                ->searchable(),
            TextColumn::make('start_date')
                ->label(__('تاريخ بدء العقد'))
                ->date('Y-m-d')
                ->searchable(),
            TextColumn::make('end_date')
                ->label(__('تاريخ انتهاء العقد'))
                ->date('Y-m-d')
                ->searchable(),
            // TextColumn::make('rent')
            //     ->label(__('الإيجار'))
            //     ->searchable(),
            TextColumn::make('total_money')
                ->label(__('المبلغ الاجمالي'))
                ->formatStateUsing(fn ($state) => number_format($state, 3)) 
                ->searchable(),
            TextColumn::make('remaining_to_finish_amount')
                ->label(__('اجمالي المتبقي'))
                ->formatStateUsing(fn ($state) => number_format($state, 3)) 
                ->searchable(),
            // TextColumn::make('remaining_payment_amount')
            //     ->label(__('اجمالي المتبقي'))
            //     ->searchable(),  
            TextColumn::make('total_remaining_amount')
                ->formatStateUsing(fn ($state) => number_format($state, 3)) 
                ->label(__('المتاخرات'))
                ->searchable(), 
        ])
        ->actions([
            ActionGroup::make([
                Action::make('single_export')
                ->label(__('استخراج'))
                ->color('success')
                ->icon('heroicon-o-document-text')
                ->action(function ($record) {
                    $exporter = new \App\Exports\SingleContractExporter([$record]);
                    return $exporter->export();
                })
                ->visible(fn ($record) => auth()->user()->can('حذف العقود')),
            EditAction::make()
                ->label(__('تعديل'))
                ->color('warning')
                ->visible(fn ($record) => auth()->user()->can('تعديل العقود')),
            DeleteAction::make()
                ->label(__('حذف'))
                ->visible(fn ($record) => auth()->user()->can('حذف العقود')),
            ])
            ->label(__('ادارة'))
            ->color(Color::Indigo)
            ->button(),

        ])
        ->headerActions([
            ExportPdfAction::make()
                ->exporter(ContractExporter::class)
                ->label(__('استخراج كافة العقود'))
                ->columnMapping(false)
                ->visible(fn ($record) => auth()->user()->can('حذف العقود')),
        ])
        ->bulkActions([
            // ExportPdfAction::make()
            //     ->exporter(ContractExporter::class)
            //     ->label(__('استخراج العقود المحددة'))
            //     ->before(function ($records) {
            //         return $records;
            //     })
            //     ->visible(fn ($record) => auth()->user()->can('حذف العقود')),
        ]);
}

public static function canViewAny(): bool
{
    return auth()->user()->can('عرض العقود');
}

public static function canCreate(): bool
{
    return auth()->user()->can('إنشاء العقود');
}

public static function canDeleteAny(): bool
{
    return auth()->user()->can('حذف العقود');
}

public static function getPages(): array
{
    return [
        'index' => Pages\ListContracts::route('/'),
        'create' => Pages\CreateContract::route('/create'),
        'edit' => Pages\EditContract::route('/{record}/edit'),
        'view' => Pages\ViewContract::route('/{record}'),
    ];
}
// protected static function beforeSave(Contract $contract)
// {

// }    

}`

How to reproduce the bug

just do as example that i told in what happend

Package Version

latest

PHP Version

8.3.10

Laravel Version

11

Which operating systems does with happen with?

Windows

Notes

No response

mohamedsabil83 commented 2 months ago

I noticed that the issue is related to the hijri-calendar extension for the dayjs library. We might replace the calendar package in the next major release unless a solution is found.

adel1415 commented 2 months ago

لدي نفس المشكلة والمشكلة الاخرى ان شهر اثنين عند الضغط على يوم 29 / 2 / 1446 يطلع 01 / 03 / 1446

mohamedsabil83 commented 2 months ago

Because the extension and process are heavily related to the Gregory calendar. PR is welcome.

CodeDanger commented 1 month ago

idk if that gonna help ya all but this how i did it for me simple one

import moment from "moment-hijri";
// import "moment-timezone";

export default function hijriDateTimePickerFormComponent({
    displayFormat,
    firstDayOfWeek,
    isAutofocused,
    locale,
    shouldCloseOnDateSelection,
    timeZone,
    state,
}) {
    // console.log(moment.tz);
    const max_allowed_year = 1500;
    const min_allowed_year = 1356;
    const timezone = timeZone;

    return {
        daysInFocusedMonth: [],

        displayFormat: displayFormat,

        displayText: "",

        emptyDaysInFocusedMonth: [],

        focusedDate: null,

        focusedMonth: null,

        focusedYear: null,

        hour: null,

        isClearingState: false,

        minute: null,

        second: null,

        state,

        dayLabels: [],

        months: [],

        init: function () {
            moment.locale(locale);

            // Initialize focusedDate using Hijri start of the date
            this.focusedDate =
                this.getSelectedDate() ?? moment().startOf("iDate");

            let date = this.getSelectedDate() ?? moment().startOf("iDate");

            if (this.getMaxDate() !== null && date.isAfter(this.getMaxDate())) {
                date = null;
            } else if (
                this.getMinDate() !== null &&
                date.isBefore(this.getMinDate())
            ) {
                date = null;
            }

            this.hour = date?.hour() ?? 0;
            this.minute = date?.minute() ?? 0;
            this.second = date?.second() ?? 0;
            // Set focused date display text
            this.setDisplayText();

            // Initialize months and day labels
            this.setMonths();
            this.setDayLabels();

            if (isAutofocused) {
                this.$nextTick(() =>
                    this.togglePanelVisibility(this.$refs.button)
                );
            }

            this.setupDateWatchers();
            this.setupTimeWatchers();
        },

        setupDateWatchers: function () {
            this.$watch("focusedMonth", (new_val, _) => {
                var new_month = parseInt(new_val);
                if (this.focusedDate.iMonth() === new_month) {
                    return;
                }

                this.focusedDate = this.focusedDate.iMonth(new_month); // Set month in Hijri

                this.setupDaysGrid();
            });

            this.$watch("focusedYear", (new_val, _) => {
                var new_year = parseInt(new_val);
                if (new_year > max_allowed_year) {
                    this.focusedYear = max_allowed_year;
                    return;
                }
                if (new_year?.toString()?.length > 4) {
                    new_year = parseInt(new_year.toString().substring(0, 4));
                }

                if (!new_year || new_year?.toString()?.length !== 4) {
                    return;
                }

                if (new_year < min_allowed_year) {
                    this.focusedYear = min_allowed_year;
                    return;
                }
                // let year = +this.focusedYear;

                if (!Number.isInteger(new_year)) {
                    new_year = moment().iYear(); // Hijri year

                    // this.focusedYear = year;
                }
                if (this.focusedDate.iYear() === new_year) {
                    return;
                }

                this.focusedDate = this.focusedDate.iYear(new_year); // Set year in Hijri
                this.setupDaysGrid();
            });

            this.$watch("focusedDate", () => {
                let month = this.focusedDate.iMonth();
                let year = this.focusedDate.iYear();
                if (this.focusedMonth !== month) {
                    this.focusedMonth = month;
                }

                if (this.focusedYear !== year) {
                    this.focusedYear = year;
                }

                this.setupDaysGrid();
            });
        },

        setupTimeWatchers: function () {
            this.$watch("hour", () => {
                this.handleTimeChange("hour", 23);
            });

            this.$watch("minute", () => {
                this.handleTimeChange("minute", 59);
            });

            this.$watch("second", () => {
                this.handleTimeChange("second", 59);
            });

            this.$watch("state", () => {
                if (this.state === undefined) {
                    return;
                }

                let date = this.getSelectedDate();

                if (date === null) {
                    this.clearState();
                    return;
                }

                if (
                    this.getMaxDate() !== null &&
                    date?.isAfter(this.getMaxDate())
                ) {
                    date = null;
                }
                if (
                    this.getMinDate() !== null &&
                    date?.isBefore(this.getMinDate())
                ) {
                    date = null;
                }

                this.syncTimeWithState(date);
            });
        },

        handleTimeChange: function (unit, max) {
            let value = +this[unit];

            if (!Number.isInteger(value)) {
                this[unit] = 0;
            } else if (value > max) {
                this[unit] = 0;
            } else if (value < 0) {
                this[unit] = max;
            }

            if (this.isClearingState) {
                return;
            }

            let date = this.getSelectedDate() ?? this.focusedDate;
            this.setState(date[unit](this[unit] ?? 0));
        },

        syncTimeWithState: function (date) {
            const newHour = date?.hour() ?? 0;
            if (this.hour !== newHour) {
                this.hour = newHour;
            }

            const newMinute = date?.minute() ?? 0;
            if (this.minute !== newMinute) {
                this.minute = newMinute;
            }

            const newSecond = date?.second() ?? 0;
            if (this.second !== newSecond) {
                this.second = newSecond;
            }

            this.setDisplayText();
        },

        clearState: function () {
            this.isClearingState = true;
            this.setState(null);

            this.hour = 0;
            this.minute = 0;
            this.second = 0;

            this.$nextTick(() => (this.isClearingState = false));
        },

        dateIsDisabled: function (date) {
            return this.isDateDisabled(date, this.$refs?.disabledDates?.value);
        },

        isDateDisabled: function (date, disabledDates) {
            if (
                disabledDates &&
                JSON.parse(disabledDates).some((disabledDate) => {
                    if (disabledDate == null) return false;
                    disabledDate = moment(disabledDate, this.displayFormat);

                    if (!disabledDate.isValid()) {
                        return false;
                    }

                    return disabledDate.isSame(date, "iDay");
                })
            ) {
                return true;
            }
            // console.log(date.isAfter(this.getMaxDate(), "iDay"));

            if (this.getMaxDate() && date.isAfter(this.getMaxDate(), "iDay")) {
                return true;
            }
            if (this.getMinDate() && date.isBefore(this.getMinDate(), "iDay")) {
                return true;
            }

            return false;
        },

        dayIsDisabled: function (day) {
            return this.dateIsDisabled(this.focusedDate.iDate(day));
        },

        dayIsSelected: function (day) {
            let selectedDate = this.getSelectedDate();

            return (
                selectedDate &&
                selectedDate.iDate() === day &&
                selectedDate.iMonth() === this.focusedDate.iMonth() &&
                selectedDate.iYear() === this.focusedDate.iYear()
            );
        },

        dayIsToday: function (day) {
            let today = moment();
            return (
                today.iDate() === day &&
                today.iMonth() === this.focusedDate.iMonth() &&
                today.iYear() === this.focusedDate.iYear()
            );
        },

        getMaxDate: function () {
            if (
                this.$refs.maxDate?.value == null ||
                this.$refs.maxDate?.value == ""
            )
                return null;
            let date = moment(this.$refs.maxDate?.value, this.displayFormat);
            return date.isValid() ? date : null;
        },

        getMinDate: function () {
            if (
                this.$refs.minDate?.value == null ||
                this.$refs.minDate?.value == ""
            )
                return null;
            let date = moment(this.$refs.minDate?.value, this.displayFormat);
            return date.isValid() ? date : null;
        },

        getSelectedDate: function () {
            if (!this.state) return null;
            let date = moment(this.state, this.displayFormat);
            return date.isValid() ? date : null;
        },

        togglePanelVisibility: function () {
            if (!this.isOpen()) {
                this.focusedDate =
                    this.getSelectedDate() ?? this.getMinDate() ?? moment();
                this.setupDaysGrid();
            }
            this.$refs.panel.toggle(this.$refs.button);
        },

        selectDate: function (day = null) {
            if (day) this.setFocusedDay(day);
            this.setState(this.focusedDate);
            if (shouldCloseOnDateSelection) this.togglePanelVisibility();
        },

        setDisplayText: function () {
            this.displayText = this.getSelectedDate()
                ? this.getSelectedDate().format(this.displayFormat)
                : "";
        },

        setMonths: function () {
            this.months =
                locale === "ar"
                    ? [
                          "محرم",
                          "صفر",
                          "ربيع الأول",
                          "ربيع الثاني",
                          "جمادى الأولى",
                          "جمادى الآخرة",
                          "رجب",
                          "شعبان",
                          "رمضان",
                          "شوال",
                          "ذو القعدة",
                          "ذو الحجة",
                      ]
                    : [
                          "Muharram",
                          "Safar",
                          "Rabi al-Awwal",
                          "Rabi al-Thani",
                          "Jumada al-Ula",
                          "Jumada al-Alkhirah",
                          "Rajab",
                          "Sha’ban",
                          "Ramadhan",
                          "Shawwal",
                          "Thul-Qi’dah",
                          "Thul-Hijjah",
                      ];
        },

        setDayLabels: function () {
            this.dayLabels = this.getDayLabels();
        },

        getDayLabels: function () {
            // const labels = dayjs.weekdaysShort();
            const labels =
                locale === "ar"
                    ? [
                          "أحد",
                          "إثنين",
                          "ثلاثاء",
                          "أربعاء",
                          "خميس",
                          "جمعة",
                          "سبت",
                      ]
                    : ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

            if (firstDayOfWeek === 0) {
                return labels;
            }

            return [
                ...labels.slice(firstDayOfWeek),
                ...labels.slice(0, firstDayOfWeek),
            ];
        },
        setupDaysGrid: function () {
            let date = this.focusedDate ?? moment().startOf("iMonth");
            let startDayOfWeek = date.startOf("iMonth").day();
            startDayOfWeek = startDayOfWeek === 0 ? 6 : startDayOfWeek - 1;
            this.emptyDaysInFocusedMonth = Array.from(
                { length: startDayOfWeek },
                (_, i) => i + 1
            );
            this.daysInFocusedMonth = Array.from(
                { length: date.iDaysInMonth() },
                (_, i) => i + 1
            );
        },

        setFocusedDay: function (day) {
            this.focusedDate = (this.focusedDate ?? moment()).iDate(day);
        },

        setState: function (date) {
            if (!date || this.dateIsDisabled(date)) return;
            // this.state = date.format("YYYY-MM-DD HH:mm:ss");
            this.state = date.format(this.displayFormat);
            this.setDisplayText();
        },

        isOpen: function () {
            return this.$refs.panel?.style.display === "block";
        },
    };
}

and this is the html

@php
    use Filament\Support\Facades\FilamentView;

    $datalistOptions = $getDatalistOptions();
    $extraAlpineAttributes = $getExtraAlpineAttributes();
    $id = $getId();
    $isDisabled = $isDisabled();
    $isPrefixInline = $isPrefixInline();
    $isSuffixInline = $isSuffixInline();
    $prefixActions = $getPrefixActions();
    $prefixIcon = $getPrefixIcon();
    $prefixLabel = $getPrefixLabel();
    $suffixActions = $getSuffixActions();
    $suffixIcon = $getSuffixIcon();
    $suffixLabel = $getSuffixLabel();
    $statePath = $getStatePath();
@endphp

<x-dynamic-component
    :component="$getFieldWrapperView()"
    :field="$field"
    :inline-label-vertical-alignment="\Filament\Support\Enums\VerticalAlignment::Center"
>
    <x-filament::input.wrapper
        :disabled="$isDisabled"
        :inline-prefix="$isPrefixInline"
        :inline-suffix="$isSuffixInline"
        :prefix="$prefixLabel"
        :prefix-actions="$prefixActions"
        :prefix-icon="$prefixIcon"
        :prefix-icon-color="$getPrefixIconColor()"
        :suffix="$suffixLabel"
        :suffix-actions="$suffixActions"
        :suffix-icon="$suffixIcon"
        :suffix-icon-color="$getSuffixIconColor()"
        :valid="! $errors->has($statePath)"
        :attributes="\Filament\Support\prepare_inherited_attributes($getExtraAttributeBag())"
    >
        <div
            x-ignore
            @if (FilamentView::hasSpaMode())
                ax-load="visible"
            @else
                ax-load
            @endif
            ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('hijri-date-picker') }}"
            x-data="hijriDateTimePickerFormComponent({
                displayFormat: '{{ $convert_format_to_moment($getDisplayFormat()) }}',
                firstDayOfWeek: {{ $getFirstDayOfWeek() }},
                isAutofocused: @js($isAutofocused()),
                locale: @js(app()->getLocale()),
                shouldCloseOnDateSelection: @js($shouldCloseOnDateSelection()),
                timeZone: '{{ env("APP_TIMEZONE","Asia/Riyadh") }}',
                state: $wire.{{ $applyStateBindingModifiers("\$entangle('{$statePath}')") }},
            })"
            x-on:keydown.esc="isOpen() && $event.stopPropagation()"
            {{
                $attributes
                    ->merge($getExtraAttributes(), escape: false)
                    ->merge($getExtraAlpineAttributes(), escape: false)
                    ->class(['filament-hijri-picker'])
            }}
        >
            <input
                x-ref="maxDate"
                type="hidden"
                value="{{ $getMaxDate() }}"
            />

            <input
                x-ref="minDate"
                type="hidden"
                value="{{ $getMinDate() }}"
            />

            <input
                x-ref="disabledDates"
                type="hidden"
                value="{{ json_encode($getDisabledDates()) }}"
            />

            <button
                x-ref="button"
                x-on:click="togglePanelVisibility()"
                x-on:keydown.enter.stop.prevent="
                    if (! $el.disabled) {
                        isOpen() ? selectDate() : togglePanelVisibility()
                    }
                "
                {{-- x-on:keydown.arrow-left.stop.prevent="if (! $el.disabled) focusPreviousDay()"
                x-on:keydown.arrow-right.stop.prevent="if (! $el.disabled) focusNextDay()"
                x-on:keydown.arrow-up.stop.prevent="if (! $el.disabled) focusPreviousWeek()"
                x-on:keydown.arrow-down.stop.prevent="if (! $el.disabled) focusNextWeek()" --}}
                x-on:keydown.backspace.stop.prevent="if (! $el.disabled) clearState()"
                x-on:keydown.clear.stop.prevent="if (! $el.disabled) clearState()"
                x-on:keydown.delete.stop.prevent="if (! $el.disabled) clearState()"
                aria-label="{{ $getPlaceholder() }}"
                type="button"
                tabindex="-1"
                @disabled($isDisabled)
                {{
                    $getExtraTriggerAttributeBag()->class([
                        'w-full',
                    ])
                }}
            >
                <input
                    @disabled($isDisabled)
                    readonly
                    placeholder="{{ $getPlaceholder() }}"
                    wire:key="{{ $this->getId() }}.{{ $statePath }}.{{ $field::class }}.display-text"
                    x-model="displayText"
                    @if ($id = $getId()) id="{{ $id }}" @endif
                    @class([
                        'w-full border-none bg-transparent px-3 py-1.5 text-base text-gray-950 outline-none transition duration-75 placeholder:text-gray-400 focus:ring-0 disabled:text-gray-500 disabled:[-webkit-text-fill-color:theme(colors.gray.500)] dark:text-white dark:placeholder:text-gray-500 dark:disabled:text-gray-400 dark:disabled:[-webkit-text-fill-color:theme(colors.gray.400)] sm:text-sm sm:leading-6',
                    ])
                />
            </button>

            <div
                x-ref="panel"
                x-cloak
                x-float.placement.bottom-start.offset.flip.shift="{ offset: 8 }"
                wire:ignore
                wire:key="{{ $this->getId() }}.{{ $statePath }}.{{ $field::class }}.panel"
                @class([
                    'filament-hijri-picker-panel absolute z-10 rounded-lg bg-white p-4 shadow-lg ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10',
                ])
            >
                <div class="grid gap-y-3">
                    @if ($hasDate())
                        <div class="flex items-center justify-between">
                            <select
                                x-model="focusedMonth"
                                class="grow cursor-pointer border-none bg-transparent p-0 text-sm font-medium text-gray-950 focus:ring-0 dark:bg-gray-900 dark:text-white"
                            >
                                <template
                                    x-for="(month, index) in months"
                                >
                                    <option
                                        x-bind:value="index"
                                        x-text="month"
                                    ></option>
                                </template>
                            </select>

                            <input
                                type="number"
                                inputmode="numeric"
                                x-model.debounce="focusedYear"
                                max="1500"
                                min="1356"
                                class="w-16 border-none bg-transparent p-0 text-right text-sm text-gray-950 focus:ring-0 dark:text-white"
                            />
                        </div>

                        <div class="grid grid-cols-7 gap-1">
                            <template
                                x-for="(day, index) in dayLabels"
                                x-bind:key="index"
                            >
                                <div
                                    x-text="day"
                                    class="text-center text-xs font-medium text-gray-500 dark:text-gray-400"
                                ></div>
                            </template>
                        </div>

                        <div
                            role="grid"
                            class="grid grid-cols-[repeat(7,minmax(theme(spacing.7),1fr))] gap-1"
                        >
                            <template
                                x-for="day in emptyDaysInFocusedMonth"
                                x-bind:key="day"
                            >
                                <div></div>
                            </template>

                            <template
                                x-for="day in daysInFocusedMonth"
                                x-bind:key="day"
                            >
                            <div
                            x-text="day"
                            x-on:click="dayIsDisabled(day) || selectDate(day)"
                            x-on:mouseenter="setFocusedDay(day)"
                            role="option"
                            x-bind:aria-selected="focusedDate.iDate() === day && focusedDate.iMonth() === focusedMonth && focusedDate.iYear() === focusedYear"
                            x-bind:class="{
                                'text-gray-950 dark:text-white': 
                                    ! dayIsToday(day) && 
                                    ! dayIsSelected(day) && 
                                    (focusedDate.iDate() !== day || focusedDate.iMonth() !== focusedMonth || focusedDate.iYear() !== focusedYear), // Ensure it's not selected unless all conditions match

                                'cursor-pointer': 
                                    ! dayIsDisabled(day),

                                'text-primary-600 dark:text-primary-400': 
                                    dayIsToday(day) &&
                                    ! dayIsSelected(day) &&
                                    focusedDate.iDate() !== day &&
                                    ! dayIsDisabled(day) &&
                                    focusedDate.iMonth() === focusedMonth && 
                                    focusedDate.iYear() === focusedYear, // Only apply if same month and year

                                'bg-gray-50 dark:bg-white/5': 
                                    focusedDate.iDate() === day && 
                                    ! dayIsSelected(day) &&
                                    focusedDate.iMonth() === focusedMonth && 
                                    focusedDate.iYear() === focusedYear, // Also check month and year

                                'text-primary-600 bg-gray-50 dark:bg-white/5 dark:text-primary-400': 
                                    dayIsSelected(day) && 
                                    focusedDate.iMonth() === focusedMonth && 
                                    focusedDate.iYear() === focusedYear, // Only selected if month and year match

                                'pointer-events-none': 
                                    dayIsDisabled(day),

                                'opacity-50': 
                                    focusedDate.iDate() !== day && 
                                    dayIsDisabled(day) &&
                                    focusedDate.iMonth() === focusedMonth && 
                                    focusedDate.iYear() === focusedYear // Apply opacity check with month and year
                            }"
                            class="rounded-full text-center text-sm leading-loose transition duration-75"
                        ></div>

                            </template>
                        </div>
                    @endif

                    @if ($hasTime())
                        <div
                            class="flex items-center justify-center rtl:flex-row-reverse"
                        >
                            <input
                                max="23"
                                min="0"
                                step="{{ $getHoursStep() }}"
                                type="number"
                                inputmode="numeric"
                                x-model.debounce="hour"
                                class="me-1 w-10 border-none bg-transparent p-0 text-center text-sm text-gray-950 focus:ring-0 dark:text-white"
                            />

                            <span
                                class="text-sm font-medium text-gray-500 dark:text-gray-400"
                            >
                                    :
                                </span>

                            <input
                                max="59"
                                min="0"
                                step="{{ $getMinutesStep() }}"
                                type="number"
                                inputmode="numeric"
                                x-model.debounce="minute"
                                class="me-1 w-10 border-none bg-transparent p-0 text-center text-sm text-gray-950 focus:ring-0 dark:text-white"
                            />

                            @if ($hasSeconds())
                                <span
                                    class="text-sm font-medium text-gray-500 dark:text-gray-400"
                                >
                                        :
                                    </span>

                                <input
                                    max="59"
                                    min="0"
                                    step="{{ $getSecondsStep() }}"
                                    type="number"
                                    inputmode="numeric"
                                    x-model.debounce="second"
                                    class="me-1 w-10 border-none bg-transparent p-0 text-center text-sm text-gray-950 focus:ring-0 dark:text-white"
                                />
                            @endif
                        </div>
                    @endif
                </div>
            </div>
        </div>
    </x-filament::input.wrapper>

    @if ($datalistOptions)
        <datalist id="{{ $id }}-list">
            @foreach ($datalistOptions as $option)
                <option value="{{ $option }}" />
            @endforeach
        </datalist>
    @endif
</x-dynamic-component>
CodeDanger commented 1 month ago

it's just temp sol till find another one

mohamedsabil83 commented 1 month ago

Thank you @CodeDanger. I'll give it a try.