From f2cc95de2628a23c1c115333b4d963715744ed94 Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Mon, 29 Apr 2024 20:33:50 +0200 Subject: [PATCH 01/12] Feature: Settings UI for time-based form goals (#7368) --- src/DonationForms/Properties/FormSettings.php | 19 +++ .../ValueObjects/GoalProgressType.php | 19 +++ .../ViewModels/FormBuilderViewModel.php | 39 +++++ .../form-builder/src/common/getWindowData.ts | 12 ++ .../src/components/DatePicker/index.tsx | 141 ++++++++++++++++++ .../src/components/DatePicker/styles.scss | 50 +++++++ .../general-controls/donation-goal/index.tsx | 97 +++++++++--- .../js/form-builder/src/types/formSettings.ts | 3 + .../ViewModels/FormBuilderViewModelTest.php | 1 + 9 files changed, 357 insertions(+), 24 deletions(-) create mode 100644 src/DonationForms/ValueObjects/GoalProgressType.php create mode 100644 src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx create mode 100644 src/FormBuilder/resources/js/form-builder/src/components/DatePicker/styles.scss diff --git a/src/DonationForms/Properties/FormSettings.php b/src/DonationForms/Properties/FormSettings.php index cd590c0e83..61061f9fe3 100644 --- a/src/DonationForms/Properties/FormSettings.php +++ b/src/DonationForms/Properties/FormSettings.php @@ -7,11 +7,13 @@ use Give\DonationForms\ValueObjects\DesignSettingsSectionStyle; use Give\DonationForms\ValueObjects\DesignSettingsTextFieldStyle; use Give\DonationForms\ValueObjects\DonationFormStatus; +use Give\DonationForms\ValueObjects\GoalProgressType; use Give\DonationForms\ValueObjects\GoalType; use Give\Framework\Support\Contracts\Arrayable; use Give\Framework\Support\Contracts\Jsonable; /** + * @unreleased Add goalProgressType * @since 3.2.0 Remove addSlashesRecursive method * @since 3.0.0 */ @@ -45,6 +47,18 @@ class FormSettings implements Arrayable, Jsonable * @var GoalType */ public $goalType; + /** + * @var GoalProgressType + */ + public $goalProgressType; + /** + * @var string + */ + public $goalStartDate; + /** + * @var string + */ + public $goalEndDate; /** * @var string */ @@ -268,6 +282,11 @@ public static function fromArray(array $array): self $self->goalType = ! empty($array['goalType']) && GoalType::isValid($array['goalType']) ? new GoalType( $array['goalType'] ) : GoalType::AMOUNT(); + $self->goalProgressType = ! empty($array['goalProgressType']) && GoalProgressType::isValid($array['goalProgressType']) + ? new GoalProgressType($array['goalProgressType']) + : GoalProgressType::ALL_TIME(); + $self->goalStartDate = $array['goalStartDate'] ?? ''; + $self->goalEndDate = $array['goalEndDate'] ?? ''; $self->designId = $array['designId'] ?? null; $self->primaryColor = $array['primaryColor'] ?? '#69b86b'; $self->secondaryColor = $array['secondaryColor'] ?? '#f49420'; diff --git a/src/DonationForms/ValueObjects/GoalProgressType.php b/src/DonationForms/ValueObjects/GoalProgressType.php new file mode 100644 index 0000000000..58da5b921a --- /dev/null +++ b/src/DonationForms/ValueObjects/GoalProgressType.php @@ -0,0 +1,19 @@ + give_get_option('agreement_text'), ], 'goalTypeOptions' => $this->getGoalTypeOptions(), + 'goalProgressOptions' => $this->getGoalProgressOptions(), 'nameTitlePrefixes' => give_get_option('title_prefixes'), 'isExcerptEnabled' => give_is_setting_enabled(give_get_option('forms_excerpt')), 'intlTelInputSettings' => IntlTelInput::getSettings(), @@ -110,6 +113,23 @@ public function getGoalTypeOption( ]; } + /** + * @unreleased + */ + public function getGoalProgressOption( + string $value, + string $label, + string $description + ): array + { + return [ + 'value' => $value, + 'label' => $label, + 'description' => $description + ]; + } + + /** * @since 3.0.0 */ @@ -158,6 +178,25 @@ public function getGoalTypeOptions(): array return $options; } + /** + * @unreleased + */ + public function getGoalProgressOptions(): array + { + return [ + $this->getGoalProgressOption( + GoalProgressType::ALL_TIME, + __('All time', 'give'), + __('Displays the goal progress for a lifetime, starting from when this form was published.', 'give') + ), + $this->getGoalProgressOption( + GoalProgressType::CUSTOM, + __('Custom', 'give'), + __('Displays the goal progress from the start date to the end date.', 'give') + ) + ]; + } + /** * @since 3.0.0 */ diff --git a/src/FormBuilder/resources/js/form-builder/src/common/getWindowData.ts b/src/FormBuilder/resources/js/form-builder/src/common/getWindowData.ts index 6fbd37cda8..807418095f 100644 --- a/src/FormBuilder/resources/js/form-builder/src/common/getWindowData.ts +++ b/src/FormBuilder/resources/js/form-builder/src/common/getWindowData.ts @@ -19,6 +19,17 @@ type GoalTypeOption = { }; /** + * @unreleased + */ +type GoalProgressOption = { + value: string; + label: string; + description: string; + isCustom: boolean; +}; + +/** + * @unreleased Added goalProgressOptions * @since 3.9.0 Added intlTelInputSettings * @since 3.7.0 Added isExcerptEnabled * @since 3.0.0 @@ -50,6 +61,7 @@ interface FormBuilderWindowData { donationConfirmationTemplateTags: TemplateTag[]; termsAndConditions: TermsAndConditions; goalTypeOptions: GoalTypeOption[]; + goalProgressOptions: GoalProgressOption[]; nameTitlePrefixes: string[]; isExcerptEnabled: boolean; intlTelInputSettings: IntlTelInputSettings; diff --git a/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx b/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx new file mode 100644 index 0000000000..1f5eaee081 --- /dev/null +++ b/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx @@ -0,0 +1,141 @@ +import {__} from '@wordpress/i18n'; +import {useRef, useState} from 'react'; +import {close} from '@wordpress/icons'; +import {Button, DatePicker, DateTimePicker, PanelRow, Popover, TextControl} from '@wordpress/components'; + +import './styles.scss'; + +interface DatePickerProps { + label: string; + placeholder: string; + date: string; + onSelect: (date: string) => void; + invalidDateBefore?: string; + invalidDateAfter?: string; + is12Hour?: boolean; + showTimeSelector?: boolean; +} + +/** + * @unreleased + */ +export default ({ + label, + placeholder, + date: value, + onSelect, + invalidDateBefore = null, + invalidDateAfter = null, + is12Hour = true, + showTimeSelector = false, +}: DatePickerProps) => { + + const popoverRef = useRef(); + + const [date, setDate] = useState(value); + const [isVisible, setIsVisible] = useState(false); + const currentDate = date ? new Date(date) : new Date(); + + + const convertJsDateToMySQLDate = (dateTime: string) => { + // split the ISO string into date and time + const [date, time] = new Date(dateTime).toISOString().split('T'); + + return `${date} ${time.slice(0, 8)}`; + }; + + const toggleVisible = () => { + setIsVisible((state) => !state); + }; + + const onSelectDate = () => { + onSelect(convertJsDateToMySQLDate(date)); + setIsVisible(false); + }; + + const checkDate = (date: Date) => { + // Check if the date is in range + if (invalidDateBefore && invalidDateAfter) { + return !(date > new Date(invalidDateBefore) && date < new Date(invalidDateAfter)); + } + + if (invalidDateBefore && date < new Date(invalidDateAfter)) { + return true; + } + + if (invalidDateAfter && date > new Date(invalidDateAfter)) { + return true; + } + + return false; + }; + + return ( + + {}} + onClick={toggleVisible} + /> + {isVisible && ( + <> + + + + + + + + + + )} + + ); +} diff --git a/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/styles.scss b/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/styles.scss new file mode 100644 index 0000000000..2f7928ac6b --- /dev/null +++ b/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/styles.scss @@ -0,0 +1,50 @@ +.givewp-date-picker { + &_input { + .components-text-control__input { + background-image: url('data:image/svg+xml,'); + background-repeat: no-repeat; + background-position: right 10px center; + } + } + + &_popover { + .components-popover__content { + min-width: 320px; + padding: 16px; + + .components-number-control { + width: unset; + } + } + + .components-input-control__input { + max-width: 50px; + } + + &__close-button { + all: unset; + position: absolute; + cursor: pointer; + top: 10px; + right: 20px; + } + + &__close-button:focus { + border: none !important; + box-shadow: none !important; + } + + &__buttons { + padding-top: 20px; + display: flex; + gap: 10px; + } + + label { + font-size: 13px; + font-weight: 600; + color: #0e0e0e; + padding-bottom: 20px; + } + } +} diff --git a/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/donation-goal/index.tsx b/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/donation-goal/index.tsx index cd403d837f..bac7255ed5 100644 --- a/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/donation-goal/index.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/donation-goal/index.tsx @@ -11,18 +11,30 @@ import { import {getFormBuilderWindowData} from '@givewp/form-builder/common/getWindowData'; import useDonationFormPubSub from '@givewp/forms/app/utilities/useDonationFormPubSub'; import {CurrencyControl} from '@givewp/form-builder/components/CurrencyControl'; +import DatePicker from '@givewp/form-builder/components/DatePicker'; -const {goalTypeOptions} = getFormBuilderWindowData(); +const {goalTypeOptions, goalProgressOptions} = getFormBuilderWindowData(); const DonationGoal = ({dispatch}) => { const { - settings: {enableDonationGoal, enableAutoClose, goalAchievedMessage, goalType, goalAmount}, + settings: { + enableDonationGoal, + enableAutoClose, + goalAchievedMessage, + goalType, + goalProgressType, + goalAmount, + goalStartDate, + goalEndDate + }, } = useFormState(); const {publishGoal, publishGoalType} = useDonationFormPubSub(); const selectedGoalType = goalTypeOptions.find((option) => option.value === goalType); const selectedGoalDescription = selectedGoalType ? selectedGoalType.description : ''; + const selectedGoalProgressType = goalProgressOptions.find((option) => option.value === goalProgressType); + const selectedGoalProgressDescription = selectedGoalProgressType ? selectedGoalProgressType.description : ''; return ( @@ -40,28 +52,6 @@ const DonationGoal = ({dispatch}) => { {enableDonationGoal && ( <> - - dispatch(setFormSettings({enableAutoClose: !enableAutoClose}))} - /> - - {enableAutoClose && ( - - - dispatch(setFormSettings({goalAchievedMessage: goalAchievedMessage})) - } - /> - - )} { /> )} + + dispatch(setFormSettings({enableAutoClose: !enableAutoClose}))} + /> + + {enableAutoClose && ( + + + dispatch(setFormSettings({goalAchievedMessage: goalAchievedMessage})) + } + /> + + )} + + { + dispatch(setFormSettings({goalProgressType})); + }} + help={selectedGoalProgressDescription} + /> + + + {goalProgressType === 'custom' && ( + <> + { + dispatch(setFormSettings({goalStartDate})); + }} + /> + + { + dispatch(setFormSettings({goalEndDate})); + }} + /> + + )} )} diff --git a/src/FormBuilder/resources/js/form-builder/src/types/formSettings.ts b/src/FormBuilder/resources/js/form-builder/src/types/formSettings.ts index bf2c5375bf..7503e446e6 100644 --- a/src/FormBuilder/resources/js/form-builder/src/types/formSettings.ts +++ b/src/FormBuilder/resources/js/form-builder/src/types/formSettings.ts @@ -15,6 +15,9 @@ export type FormSettings = { goalAchievedMessage: string; registrationNotification: boolean; goalType: string; + goalProgressType: string; + goalStartDate: string; + goalEndDate: string; goalAmount: number; designId: string; heading: string; diff --git a/tests/Unit/ViewModels/FormBuilderViewModelTest.php b/tests/Unit/ViewModels/FormBuilderViewModelTest.php index 1a88c182fa..34895f2fc7 100644 --- a/tests/Unit/ViewModels/FormBuilderViewModelTest.php +++ b/tests/Unit/ViewModels/FormBuilderViewModelTest.php @@ -87,6 +87,7 @@ public function testShouldReturnStorageData() 'agreementText' => give_get_option('agreement_text'), ], 'goalTypeOptions' => $viewModel->getGoalTypeOptions(), + 'goalProgressOptions' => $viewModel->getGoalProgressOptions(), 'nameTitlePrefixes' => give_get_option('title_prefixes'), 'isExcerptEnabled' => give_is_setting_enabled(give_get_option('forms_excerpt')), 'intlTelInputSettings' => IntlTelInput::getSettings(), From e3e86ef164b042eb955f972dc5f1817912fa8fcf Mon Sep 17 00:00:00 2001 From: "Kyle B. Johnson" Date: Thu, 2 May 2024 03:00:11 -0400 Subject: [PATCH 02/12] Refactor: Update goal/stats to use Donation Query (#7376) --- .../DonationFormGoalData.php | 20 ++++- src/DonationForms/DonationQuery.php | 79 +++++++++++++++++++ .../Repositories/DonationFormRepository.php | 39 +++++---- .../ViewModels/DonationFormViewModel.php | 17 ++-- 4 files changed, 134 insertions(+), 21 deletions(-) create mode 100644 src/DonationForms/DonationQuery.php diff --git a/src/DonationForms/DataTransferObjects/DonationFormGoalData.php b/src/DonationForms/DataTransferObjects/DonationFormGoalData.php index 77f3ab5fc9..44e4660406 100644 --- a/src/DonationForms/DataTransferObjects/DonationFormGoalData.php +++ b/src/DonationForms/DataTransferObjects/DonationFormGoalData.php @@ -4,6 +4,7 @@ use Give\DonationForms\Properties\FormSettings; use Give\DonationForms\Repositories\DonationFormRepository; +use Give\DonationForms\ValueObjects\GoalProgressType; use Give\DonationForms\ValueObjects\GoalType; use Give\Framework\Support\Contracts\Arrayable; @@ -32,6 +33,18 @@ class DonationFormGoalData implements Arrayable * @var int */ public $targetAmount; + /** + * @var GoalProgressType + */ + public $goalProgressType; + /** + * @var string|null + */ + public $goalStartDate; + /** + * @var string|null + */ + public $goalEndDate; /** * @since 3.0.0 @@ -43,6 +56,9 @@ public function __construct(int $formId, FormSettings $formSettings) $this->isEnabled = $formSettings->enableDonationGoal ?? false; $this->goalType = $formSettings->goalType ?? GoalType::AMOUNT(); $this->targetAmount = $this->formSettings->goalAmount ?? 0; + $this->goalProgressType = $this->formSettings->goalProgressType ?? GoalProgressType::ALL_TIME(); + $this->goalStartDate = $this->formSettings->goalStartDate ?? null; + $this->goalEndDate = $this->formSettings->goalEndDate ?? null; } /** @@ -68,7 +84,9 @@ public function getCurrentAmount() return $donationFormRepository->getTotalNumberOfDonorsFromSubscriptions($this->formId); case GoalType::AMOUNT(): default: - return $donationFormRepository->getTotalRevenue($this->formId); + return $this->goalProgressType->isAllTime() + ? $donationFormRepository->getTotalRevenue($this->formId) + : $donationFormRepository->getTotalRevenueForDateRange($this->formId,$this->goalStartDate, $this->goalEndDate); endswitch; } diff --git a/src/DonationForms/DonationQuery.php b/src/DonationForms/DonationQuery.php new file mode 100644 index 0000000000..b4ed4ac58f --- /dev/null +++ b/src/DonationForms/DonationQuery.php @@ -0,0 +1,79 @@ +form(1816) + * ->between('2024-02-00', '2024-02-23') + * ->sumIntendedAmount(); + */ +class DonationQuery extends QueryBuilder +{ + /** + * @unreleased + */ + public function __construct() + { + $this->from('posts', 'donation'); + } + + /** + * An opinionated join method for the donation meta table. + * @unreleased + */ + public function joinMeta($key, $alias) + { + $this->join(function (JoinQueryBuilder $builder) use ($key, $alias) { + $builder + ->leftJoin('give_donationmeta', $alias) + ->on('donation.ID', $alias . '.donation_id') + ->andOn($alias . '.meta_key', $key, true); + }); + return $this; + } + + /** + * An opinionated where method for the donation form ID meta field. + * @unreleased + */ + public function form($formId) + { + $this->joinMeta('_give_payment_form_id', 'formId'); + $this->where('formId.meta_value', $formId); + return $this; + } + + /** + * An opinionated whereBetween method for the completed date meta field. + * @unreleased + */ + public function between($startDate, $endDate) + { + $this->joinMeta('_give_completed_date', 'completed'); + $this->whereBetween('completed.meta_value', $startDate, $endDate); + return $this; + } + + /** + * Returns a calculated sum of the intended amounts (without recovered fees) for the donations. + * @unreleased + * @return int|float + */ + public function sumIntendedAmount() + { + $this->joinMeta('_give_payment_total', 'amount'); + $this->joinMeta('_give_fee_donation_amount', 'intendedAmount'); + return $this->sum( + 'COALESCE(intendedAmount.meta_value, amount.meta_value)' + ); + } +} diff --git a/src/DonationForms/Repositories/DonationFormRepository.php b/src/DonationForms/Repositories/DonationFormRepository.php index 352cd0c17c..60229fbe38 100644 --- a/src/DonationForms/Repositories/DonationFormRepository.php +++ b/src/DonationForms/Repositories/DonationFormRepository.php @@ -4,6 +4,7 @@ use Closure; use Give\DonationForms\Actions\ConvertDonationFormBlocksToFieldsApi; +use Give\DonationForms\DonationQuery; use Give\DonationForms\Models\DonationForm; use Give\DonationForms\ValueObjects\DonationFormMetaKeys; use Give\Donations\ValueObjects\DonationMetaKeys; @@ -393,10 +394,16 @@ public function getTotalNumberOfDonorsFromSubscriptions(int $formId): int */ public function getTotalNumberOfDonations(int $formId): int { - return DB::table('posts') - ->leftJoin('give_donationmeta', 'ID', 'donation_id') - ->where('meta_key', DonationMetaKeys::FORM_ID) - ->where('meta_value', $formId) + return (new DonationQuery) + ->form($formId) + ->count(); + } + + public function getTotalNumberOfDonationsForDateRange(int $formId, string $startDate, string $endDate): int + { + return (new DonationQuery) + ->form($formId) + ->between($startDate, $endDate) ->count(); } @@ -411,21 +418,25 @@ public function getTotalNumberOfSubscriptions(int $formId): int } /** + * @unreleased Update query to use intended amounts (without recovered fees). * @since 3.0.0 */ public function getTotalRevenue(int $formId): int { - $query = DB::table('give_formmeta') - ->select('meta_value as totalRevenue') - ->where('form_id', $formId) - ->where('meta_key', '_give_form_earnings') - ->get(); - - if (!$query) { - return 0; - } + return (int) (new DonationQuery) + ->form($formId) + ->sumIntendedAmount(); + } - return (int)$query->totalRevenue; + /** + * @unreleased + */ + public function getTotalRevenueForDateRange(int $formId, string $startDate, string $endDate): int + { + return (int) (new DonationQuery) + ->form($formId) + ->between($startDate, $endDate) + ->sumIntendedAmount(); } /** diff --git a/src/DonationForms/ViewModels/DonationFormViewModel.php b/src/DonationForms/ViewModels/DonationFormViewModel.php index ada297efb4..f56fc1da47 100644 --- a/src/DonationForms/ViewModels/DonationFormViewModel.php +++ b/src/DonationForms/ViewModels/DonationFormViewModel.php @@ -6,6 +6,7 @@ use Give\DonationForms\Actions\GenerateDonateRouteUrl; use Give\DonationForms\Actions\GenerateDonationFormValidationRouteUrl; use Give\DonationForms\DataTransferObjects\DonationFormGoalData; +use Give\DonationForms\DonationQuery; use Give\DonationForms\Properties\FormSettings; use Give\DonationForms\Repositories\DonationFormRepository; use Give\DonationForms\ValueObjects\GoalType; @@ -181,14 +182,18 @@ private function formStatsData(): array { $goalType = $this->goalType(); - $totalRevenue = $this->getTotalRevenue($goalType); - $totalCountValue = $this->getTotalCountValue($goalType); - $totalCountLabel = $this->getCountLabel($goalType); + $donationQuery = (new DonationQuery)->form($this->donationFormId); + + if($this->formSettings->goalProgressType->isCustom()) { + $donationQuery->between($this->formSettings->goalStartDate, $this->formSettings->goalEndDate); + } return [ - 'totalRevenue' => $totalRevenue, - 'totalCountValue' => $totalCountValue, - 'totalCountLabel' => $totalCountLabel, + 'totalRevenue' => $donationQuery->sumIntendedAmount(), + 'totalCountValue' => $goalType->isDonations() || $goalType->isAmount() + ? $donationQuery->count() + : $this->getTotalCountValue($goalType), + 'totalCountLabel' => $this->getCountLabel($goalType), ]; } From 7b1b2e214a19a2636494bf814461b8fbd9494174 Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Sat, 4 May 2024 10:46:01 +0200 Subject: [PATCH 03/12] Feature: Custom goal progress notice (#7382) --- .../Routes/RegisterFormBuilderPageRoute.php | 7 +- src/FormBuilder/ServiceProvider.php | 4 + .../src/components/DatePicker/index.tsx | 2 +- .../general-controls/donation-goal/index.tsx | 77 +++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/FormBuilder/Routes/RegisterFormBuilderPageRoute.php b/src/FormBuilder/Routes/RegisterFormBuilderPageRoute.php index 71e1c7aa3c..7904d9ffdb 100644 --- a/src/FormBuilder/Routes/RegisterFormBuilderPageRoute.php +++ b/src/FormBuilder/Routes/RegisterFormBuilderPageRoute.php @@ -152,6 +152,11 @@ public function renderPage() ), ]); + wp_localize_script('@givewp/form-builder/script', 'goalNotificationData', [ + 'actionUrl' => admin_url('admin-ajax.php?action=givewp_goal_hide_notice'), + 'isDismissed' => get_user_meta(get_current_user_id(), 'givewp-goal-notice-dismissed', true), + ]); + View::render('FormBuilder.admin-form-builder'); } @@ -165,7 +170,7 @@ public function renderPage() public function loadGutenbergScripts() { wp_enqueue_editor(); - + // Gutenberg scripts wp_enqueue_script('wp-block-library'); wp_enqueue_script('wp-format-library'); diff --git a/src/FormBuilder/ServiceProvider.php b/src/FormBuilder/ServiceProvider.php index 51a66efbfa..507d26d29d 100644 --- a/src/FormBuilder/ServiceProvider.php +++ b/src/FormBuilder/ServiceProvider.php @@ -84,5 +84,9 @@ protected function setupOnboardingTour() add_action('wp_ajax_givewp_transfer_hide_notice', static function () { give_update_meta((int)$_GET['formId'], 'givewp-form-builder-transfer-hide-notice', time(), true); }); + + add_action('wp_ajax_givewp_goal_hide_notice', static function () { + add_user_meta(get_current_user_id(), 'givewp-goal-notice-dismissed', time(), true); + }); } } diff --git a/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx b/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx index 1f5eaee081..a2b4c02a3e 100644 --- a/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx @@ -59,7 +59,7 @@ export default ({ return !(date > new Date(invalidDateBefore) && date < new Date(invalidDateAfter)); } - if (invalidDateBefore && date < new Date(invalidDateAfter)) { + if (invalidDateBefore && date < new Date(invalidDateBefore)) { return true; } diff --git a/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/donation-goal/index.tsx b/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/donation-goal/index.tsx index bac7255ed5..45ea1570a3 100644 --- a/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/donation-goal/index.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/donation-goal/index.tsx @@ -1,3 +1,6 @@ +import {CSSProperties, useState} from 'react'; +import {Icon} from '@wordpress/components'; +import {external, close} from '@wordpress/icons'; import {setFormSettings, useFormState} from '@givewp/form-builder/stores/form-state'; import {__} from '@wordpress/i18n'; import { @@ -13,6 +16,13 @@ import useDonationFormPubSub from '@givewp/forms/app/utilities/useDonationFormPu import {CurrencyControl} from '@givewp/form-builder/components/CurrencyControl'; import DatePicker from '@givewp/form-builder/components/DatePicker'; +declare const window: { + goalNotificationData: { + actionUrl: string; + isDismissed: boolean; + }; +} & Window; + const {goalTypeOptions, goalProgressOptions} = getFormBuilderWindowData(); const DonationGoal = ({dispatch}) => { @@ -30,12 +40,46 @@ const DonationGoal = ({dispatch}) => { } = useFormState(); const {publishGoal, publishGoalType} = useDonationFormPubSub(); + const [showNotice, setShowNotice] = useState(!window.goalNotificationData.isDismissed); const selectedGoalType = goalTypeOptions.find((option) => option.value === goalType); const selectedGoalDescription = selectedGoalType ? selectedGoalType.description : ''; const selectedGoalProgressType = goalProgressOptions.find((option) => option.value === goalProgressType); const selectedGoalProgressDescription = selectedGoalProgressType ? selectedGoalProgressType.description : ''; + const noticeStyles = { + container: { + position: 'relative', + display: 'flex', + flexDirection: 'column', + gap: 8, + padding: 12, + borderRadius: 2, + backgroundColor: '#f2f2f2', + color: '#0e0e0e', + fontSize: 12, + }, + title: { + fontWeight: 600, + }, + closeIcon: { + cursor: 'pointer', + height: 16, + width: 16, + position: 'absolute', + right: 12, + top: 12, + }, + externalIcon: { + height: 18, + width: 18, + fill: '#2271b1', + float: 'left', + marginTop: 2, + marginRight: 8, + } + }; + return ( @@ -144,6 +188,39 @@ const DonationGoal = ({dispatch}) => { dispatch(setFormSettings({goalEndDate})); }} /> + + {showNotice && ( + +
+ + {__('What is custom goal progress?', 'give')} + { + fetch(window.goalNotificationData.actionUrl, {method: 'POST'}) + .then(() => { + setShowNotice(false); + }); + }} + /> + + + {__('You can now set a time frame to show progress toward your goal.', 'give')} + + + {/*todo: add link to docs*/} + + + {__('Learn more about how to use the custom goal progress.', 'give')} + + +
+
+ )} )} From 0314393a1300b054301a83ca14d10263631dafe7 Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Mon, 6 May 2024 17:57:50 +0200 Subject: [PATCH 04/12] Feature: Update all form goal blocks/shortcodes to respect the new v3 fields/queries (#7380) Co-authored-by: Ante Laca --- includes/shortcodes.php | 5 ++++- src/DonationForms/DonationQuery.php | 18 +++++++++++++++++- src/MultiFormGoals/ProgressBar/Model.php | 7 +++---- templates/shortcode-goal.php | 21 ++++++++++++++++++++- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/includes/shortcodes.php b/includes/shortcodes.php index b297901a01..bdba978969 100644 --- a/includes/shortcodes.php +++ b/includes/shortcodes.php @@ -213,6 +213,7 @@ function give_form_shortcode( $atts ) { * * Show the Give donation form goals. * + * @unreleased add start_date and end_date attributes * @since 3.7.0 Sanitize attributes * @since 3.4.0 Add additional validations to check if the form is valid and has the 'published' status. * @since 1.0 @@ -228,7 +229,9 @@ function give_goal_shortcode( $atts ) { 'id' => '', 'show_text' => true, 'show_bar' => true, - 'color' => '', + 'color' => '#66BB6A', + 'start_date' => '', + 'end_date' => '', ], $atts, 'give_goal' diff --git a/src/DonationForms/DonationQuery.php b/src/DonationForms/DonationQuery.php index b4ed4ac58f..a89e53c998 100644 --- a/src/DonationForms/DonationQuery.php +++ b/src/DonationForms/DonationQuery.php @@ -52,6 +52,18 @@ public function form($formId) return $this; } + + /** + * An opinionated where method for the multiple donation form IDs meta field. + * @unreleased + */ + public function forms(array $formIds) + { + $this->joinMeta('_give_payment_form_id', 'formId'); + $this->whereIn('formId.meta_value', $formIds); + return $this; + } + /** * An opinionated whereBetween method for the completed date meta field. * @unreleased @@ -59,7 +71,11 @@ public function form($formId) public function between($startDate, $endDate) { $this->joinMeta('_give_completed_date', 'completed'); - $this->whereBetween('completed.meta_value', $startDate, $endDate); + $this->whereBetween( + 'completed.meta_value', + date('Y-m-d H:i:s', strtotime($startDate)), + date('Y-m-d H:i:s', strtotime($endDate)) + ); return $this; } diff --git a/src/MultiFormGoals/ProgressBar/Model.php b/src/MultiFormGoals/ProgressBar/Model.php index 22b1b87db2..53be5ef87d 100644 --- a/src/MultiFormGoals/ProgressBar/Model.php +++ b/src/MultiFormGoals/ProgressBar/Model.php @@ -2,6 +2,7 @@ namespace Give\MultiFormGoals\ProgressBar; +use Give\DonationForms\DonationQuery; use Give\ValueObjects\Money; class Model @@ -132,14 +133,12 @@ public function getDonationRevenueResults() /** * Get raw earnings value for Progress Bar * + * @unreleased use DonationQuery * @since 2.9.0 */ public function getTotal(): string { - $query = new Query($this->getForms()); - $results = $query->getResults(); - - return Money::ofMinor($results->total, give_get_option('currency'))->getAmount(); + return (new DonationQuery())->forms($this->getForms())->sumIntendedAmount(); } /** diff --git a/templates/shortcode-goal.php b/templates/shortcode-goal.php index ba977a1645..b23dd8c144 100644 --- a/templates/shortcode-goal.php +++ b/templates/shortcode-goal.php @@ -1,6 +1,7 @@ form($form->ID); + +if ($args['start_date'] === $args['end_date']) { + $form_income = $donationQuery->sumIntendedAmount(); +} else { + // If end date is not set, we have to use the current datetime. + if ( ! $args['end_date']) { + $args['end_date'] = date('Y-m-d H:i:s'); + } + + $form_income = $donationQuery->between($args['start_date'], $args['end_date'])->sumIntendedAmount(); +} + /** * Allow filtering the goal stats used for this shortcode context. * @@ -49,7 +68,7 @@ $shortcode_stats = apply_filters( 'give_goal_shortcode_stats', array( - 'income' => $form->get_earnings(), + 'income' => $form_income, 'goal' => $goal_progress_stats['raw_goal'], ), $form_id, From db868adc0428c9cf29617a9d9d8e3e4b6b3886c3 Mon Sep 17 00:00:00 2001 From: "Kyle B. Johnson" Date: Tue, 7 May 2024 09:08:22 -0400 Subject: [PATCH 05/12] Refactor: Simplify goal data to use query builder directly (#7385) --- .../DonationFormGoalData.php | 28 +++++--- src/DonationForms/DonationQuery.php | 7 ++ .../Repositories/DonationFormRepository.php | 19 ------ src/DonationForms/SubscriptionQuery.php | 67 +++++++++++++++++++ 4 files changed, 92 insertions(+), 29 deletions(-) create mode 100644 src/DonationForms/SubscriptionQuery.php diff --git a/src/DonationForms/DataTransferObjects/DonationFormGoalData.php b/src/DonationForms/DataTransferObjects/DonationFormGoalData.php index 44e4660406..477cce01d7 100644 --- a/src/DonationForms/DataTransferObjects/DonationFormGoalData.php +++ b/src/DonationForms/DataTransferObjects/DonationFormGoalData.php @@ -2,8 +2,11 @@ namespace Give\DonationForms\DataTransferObjects; +use Give\DonationForms\DonationQuery; +use Give\DonationForms\Models\DonationForm; use Give\DonationForms\Properties\FormSettings; use Give\DonationForms\Repositories\DonationFormRepository; +use Give\DonationForms\SubscriptionQuery; use Give\DonationForms\ValueObjects\GoalProgressType; use Give\DonationForms\ValueObjects\GoalType; use Give\Framework\Support\Contracts\Arrayable; @@ -68,25 +71,30 @@ public function __construct(int $formId, FormSettings $formSettings) */ public function getCurrentAmount() { - /** @var DonationFormRepository $donationFormRepository */ - $donationFormRepository = give(DonationFormRepository::class); + $query = $this->goalType->isOneOf(GoalType::SUBSCRIPTIONS(), GoalType::AMOUNT_FROM_SUBSCRIPTIONS(), GoalType::DONORS_FROM_SUBSCRIPTIONS()) + ? new SubscriptionQuery() + : new DonationQuery(); + + $query->form($this->formId); + + if($this->goalProgressType->isCustom()) { + $query->between($this->goalStartDate, $this->goalEndDate); + } switch ($this->goalType): case GoalType::DONORS(): - return $donationFormRepository->getTotalNumberOfDonors($this->formId); + return $query->countDonors(); case GoalType::DONATIONS(): - return $donationFormRepository->getTotalNumberOfDonations($this->formId); + return $query->count(); case GoalType::SUBSCRIPTIONS(): - return $donationFormRepository->getTotalNumberOfSubscriptions($this->formId); + return $query->count(); case GoalType::AMOUNT_FROM_SUBSCRIPTIONS(): - return $donationFormRepository->getTotalInitialAmountFromSubscriptions($this->formId); + return $query->sumInitialAmount(); case GoalType::DONORS_FROM_SUBSCRIPTIONS(): - return $donationFormRepository->getTotalNumberOfDonorsFromSubscriptions($this->formId); + return $query->countDonors(); case GoalType::AMOUNT(): default: - return $this->goalProgressType->isAllTime() - ? $donationFormRepository->getTotalRevenue($this->formId) - : $donationFormRepository->getTotalRevenueForDateRange($this->formId,$this->goalStartDate, $this->goalEndDate); + return $query->sumIntendedAmount(); endswitch; } diff --git a/src/DonationForms/DonationQuery.php b/src/DonationForms/DonationQuery.php index a89e53c998..d68e8f6d8f 100644 --- a/src/DonationForms/DonationQuery.php +++ b/src/DonationForms/DonationQuery.php @@ -2,6 +2,7 @@ namespace Give\DonationForms; +use Give\Donations\ValueObjects\DonationMetaKeys; use Give\Framework\QueryBuilder\JoinQueryBuilder; use Give\Framework\QueryBuilder\QueryBuilder; @@ -92,4 +93,10 @@ public function sumIntendedAmount() 'COALESCE(intendedAmount.meta_value, amount.meta_value)' ); } + + public function countDonors() + { + $this->joinMeta(DonationMetaKeys::DONOR_ID, 'donorId'); + return $this->count('DISTINCT donorId.meta_value'); + } } diff --git a/src/DonationForms/Repositories/DonationFormRepository.php b/src/DonationForms/Repositories/DonationFormRepository.php index 60229fbe38..542776c178 100644 --- a/src/DonationForms/Repositories/DonationFormRepository.php +++ b/src/DonationForms/Repositories/DonationFormRepository.php @@ -399,14 +399,6 @@ public function getTotalNumberOfDonations(int $formId): int ->count(); } - public function getTotalNumberOfDonationsForDateRange(int $formId, string $startDate, string $endDate): int - { - return (new DonationQuery) - ->form($formId) - ->between($startDate, $endDate) - ->count(); - } - /** * @since 3.0.0 */ @@ -428,17 +420,6 @@ public function getTotalRevenue(int $formId): int ->sumIntendedAmount(); } - /** - * @unreleased - */ - public function getTotalRevenueForDateRange(int $formId, string $startDate, string $endDate): int - { - return (int) (new DonationQuery) - ->form($formId) - ->between($startDate, $endDate) - ->sumIntendedAmount(); - } - /** * @since 3.0.0 * @return int|float diff --git a/src/DonationForms/SubscriptionQuery.php b/src/DonationForms/SubscriptionQuery.php new file mode 100644 index 0000000000..086110784e --- /dev/null +++ b/src/DonationForms/SubscriptionQuery.php @@ -0,0 +1,67 @@ +from('give_subscriptions'); + } + + /** + * @unreleased + */ + public function form($formId) + { + $this->where('product_id', $formId); + return $this; + } + + + /** + * @unreleased + */ + public function forms(array $formIds) + { + $this->whereIn('product_id', $formIds); + return $this; + } + + /** + * @unreleased + */ + public function between($startDate, $endDate) + { + $this->whereBetween( + 'created', + date('Y-m-d H:i:s', strtotime($startDate)), + date('Y-m-d H:i:s', strtotime($endDate)) + ); + return $this; + } + + /** + * @unreleased + */ + public function sumInitialAmount() + { + return $this->sum('initial_amount'); + } + + /** + * @unreleased + */ + public function countDonors() + { + return $this->count('DISTINCT customer_id'); + } +} From f4e6304b943beecaf1b5d39e34287e6095343f6d Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 8 May 2024 16:16:13 -0400 Subject: [PATCH 06/12] chore: update readmore link --- .../settings/design/general-controls/donation-goal/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/donation-goal/index.tsx b/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/donation-goal/index.tsx index 45ea1570a3..05e6f3b819 100644 --- a/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/donation-goal/index.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/donation-goal/index.tsx @@ -209,8 +209,7 @@ const DonationGoal = ({dispatch}) => { {__('You can now set a time frame to show progress toward your goal.', 'give')} - {/*todo: add link to docs*/} - + Date: Mon, 13 May 2024 13:04:19 +0200 Subject: [PATCH 07/12] fix: date --- .../form-builder/src/components/DatePicker/index.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx b/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx index a2b4c02a3e..58892ef8a7 100644 --- a/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx @@ -34,14 +34,10 @@ export default ({ const [date, setDate] = useState(value); const [isVisible, setIsVisible] = useState(false); - const currentDate = date ? new Date(date) : new Date(); - const convertJsDateToMySQLDate = (dateTime: string) => { - // split the ISO string into date and time - const [date, time] = new Date(dateTime).toISOString().split('T'); - - return `${date} ${time.slice(0, 8)}`; + const [date, time] = dateTime.split('T'); + return `${date} ${time}`; }; const toggleVisible = () => { @@ -101,7 +97,7 @@ export default ({ {showTimeSelector ? ( checkDate(date)} onChange={(date) => { setDate(date); @@ -109,7 +105,7 @@ export default ({ /> ) : ( checkDate(date)} onChange={(date) => { setDate(date); From ad2bbb364335b362ef7dff7662d9f9bd216334ef Mon Sep 17 00:00:00 2001 From: alaca Date: Tue, 14 May 2024 10:17:00 +0200 Subject: [PATCH 08/12] fix: date range --- src/DonationForms/DonationQuery.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/DonationForms/DonationQuery.php b/src/DonationForms/DonationQuery.php index d68e8f6d8f..1efea4f6dd 100644 --- a/src/DonationForms/DonationQuery.php +++ b/src/DonationForms/DonationQuery.php @@ -71,12 +71,15 @@ public function forms(array $formIds) */ public function between($startDate, $endDate) { + // If the dates are empty or invalid, they will fallback to January 1st, 1970. + // For the start date, this is exactly what we need, but for the end date, we should set it as the current date so that we have a correct date range. + $startDate = date('Y-m-d H:i:s', strtotime($startDate)); + $endDate = empty($endDate) + ? date('Y-m-d H:i:s') + : date('Y-m-d H:i:s', strtotime($endDate)); + $this->joinMeta('_give_completed_date', 'completed'); - $this->whereBetween( - 'completed.meta_value', - date('Y-m-d H:i:s', strtotime($startDate)), - date('Y-m-d H:i:s', strtotime($endDate)) - ); + $this->whereBetween('completed.meta_value', $startDate, $endDate); return $this; } From 9f3912663483c738f92247aba19d7bba10275e27 Mon Sep 17 00:00:00 2001 From: alaca Date: Tue, 14 May 2024 10:38:47 +0200 Subject: [PATCH 09/12] fix: styling discrepancies between chrome and ff --- .../js/form-builder/src/components/DatePicker/index.tsx | 1 + .../js/form-builder/src/components/DatePicker/styles.scss | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx b/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx index 58892ef8a7..5a7e9dfe10 100644 --- a/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/components/DatePicker/index.tsx @@ -84,6 +84,7 @@ export default ({ anchor={popoverRef.current} position="middle left bottom" className="givewp-date-picker_popover" + offset={window?.chrome ? 30 : 0} >