Skip to content

Add Dynamic Button for PU Activities using shortcode: Proceed to Office Visit or Material Contribution#357

Closed
nishant22029 wants to merge 65 commits intodevelopfrom
feat/add-redirection-button-for-pu-flow
Closed

Add Dynamic Button for PU Activities using shortcode: Proceed to Office Visit or Material Contribution#357
nishant22029 wants to merge 65 commits intodevelopfrom
feat/add-redirection-button-for-pu-flow

Conversation

@nishant22029
Copy link
Copy Markdown
Collaborator

@nishant22029 nishant22029 commented Oct 10, 2024

Target Issue: #78

This PR introduces a shortcode, goonj_pu_activity_button, which dynamically displays the appropriate button, either Proceed to Office Visit or Proceed to Material Contribution based on individual activity. If both activities (Office Visit and Material Contribution) have already been completed for the day, no button will be displayed.

Shortcode Implementation (goonj_pu_activity_button):

  • Fetches activity details such as source_contact_id, Office_Visit.Goonj_Processing_Center, Material_Contribution.Goonj_Office, and activity_type_id:label using the goonj_fetch_pu_activity_details function.
    These details are used to determine which button to display, or whether no button should be shown.

  • Goonj Office ID Fetching :goonj_get_goonj_office_id using this function correct Goonj office ID based on the type of activity completed:
    For Office Visit activities, the office ID is fetched from the Office_Visit.Goonj_Processing_Center field.
    For Material Contribution activities, it is retrieved from the Material_Contribution.Goonj_Office field.

  • Activity Fetching : Using this fetch_contact_pu_activities_for_today function all activities are fetched that completed by the contact for the current day.

  • Processing Activities : This function goonj_process_pu_activities Processes and analyzes which activities the user has completed, determining which button (if any) should be displayed.

  • Activity Type Check : This function check_if_both_activity_types_exist checks whether both Office Visit and Material Contribution activities have been completed. If both are done, no button will be displayed.

  • Button Generation : This function goonj_generate_activity_button Generates the appropriate button, including the correct label and URL parameters, based on the contact’s activity status.

  • Template Refactor : Refactored the existing goonj_generate_volunteer_html_button function to reuse HTML content in the newly added goonj_generate_button_html function. The button HTML is now added in the button-template.php file, improving reusability and consistency across both functions.

Screenshot 2024-10-10 at 4 50 16 PM Screenshot 2024-10-10 at 4 51 17 PM

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced new shortcodes for user interactions: goonj_pu_activity_button, goonj_contribution_volunteer_signup_button, goonj_collection_landing_page, and goonj_collection_camp_past.
    • Enhanced user redirection logic, directing logged-in users to the CiviCRM dashboard and handling failed login attempts.
    • Implemented login form validation and custom password reset handling.
    • Added functionality for processing and sending reminders related to collection camps and volunteer feedback.
  • Bug Fixes

    • Enhanced error handling and logging for user activities and redirection logic.
  • Refactor

    • Updated function names and signatures for improved usability and flexibility.
    • Enhanced script and style enqueueing for better front-end and admin experience.
  • Chores

    • Registered additional query variables for enhanced URL parameter handling.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 10, 2024

Walkthrough

The changes in this pull request introduce significant enhancements to the Goonj CRM theme, particularly within the functions.php, helpers.php, and shortcodes.php files. Key updates include the addition of new shortcodes for improved user interaction, enhanced user identification and redirection logic, and the introduction of new classes and methods for managing feedback and reminders. Additionally, the codebase has been refactored to improve maintainability and clarity, with new traits and error handling mechanisms integrated throughout.

Changes

File Path Change Summary
wp-content/themes/goonj-crm/functions.php - Added shortcodes: goonj_pu_activity_button, goonj_check_user_form, goonj_volunteer_message, goonj_contribution_volunteer_signup_button, goonj_collection_landing_page, goonj_collection_camp_past
- Defined functions for script and style enqueuing: goonj_enqueue_scripts, goonj_enqueue_admin_scripts
- Implemented user redirection: goonj_redirect_logged_in_user_to_civi_dashboard, goonj_custom_login_failed_redirect
- Added login validation: goonj_check_empty_login_fields, goonj_login_form_validation_errors
- Managed password reset with: goonj_custom_reset_password_form, goonj_custom_password_reset_redirection
- Enhanced user identification: goonj_handle_user_identification_form
- Renamed and modified button generation function to goonj_generate_button_html
- Improved error handling and logging throughout new functions
- Updated query variable registration in goonj_query_vars
wp-content/themes/goonj-crm/engine/helpers.php - Created new file helpers.php with functions for managing activity details: goonj_fetch_pu_activity_details, goonj_get_goonj_office_id, goonj_fetch_contact_pu_activities_for_today, goonj_process_pu_activities, goonj_check_if_both_pu_activity_types_exist, goonj_generate_activity_button.
wp-content/themes/goonj-crm/engine/shortcodes.php - Added shortcodes: goonj_check_user_form, goonj_volunteer_message, goonj_contribution_volunteer_signup_button, goonj_pu_activity_button, goonj_collection_landing_page, goonj_collection_camp_past
- Defined functions for shortcodes including goonj_check_user_action, goonj_generate_button_html, goonj_contribution_volunteer_signup_button, goonj_pu_activity_button, goonj_collection_camp_landing_page, goonj_collection_camp_past_data.
wp-content/civi-extensions/goonjcustom/Civi/CollectionCampOutcomeService.php - Introduced CollectionCampOutcomeService class with methods for processing camp reminders and sending outcome reminder emails.
wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php - Updated CollectionCampService class to utilize new traits and refactor QR code generation logic.
wp-content/civi-extensions/goonjcustom/Civi/CollectionCampVolunteerFeedbackService.php - Added CollectionCampVolunteerFeedbackService class for managing volunteer feedback reminders.
wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php - Enhanced DroppingCenterService class with QR code generation functionality.
wp-content/civi-extensions/goonjcustom/Civi/HelperService.php - Created HelperService class to retrieve default email information.
wp-content/civi-extensions/goonjcustom/Civi/Traits/CollectionSource.php - Introduced CollectionSource trait for caching subtype IDs and related methods.
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php - Added QrCodeable trait with methods for QR code generation and saving.
wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/CollectionCampOutcomeReminderCron.php - Introduced API endpoint for camp outcome reminders with specified functions.
wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/VolunteerFeedbackReminderCron.php - Introduced API for managing volunteer feedback reminders with specified functions.

Possibly related PRs

🌟 In the realm of code, new features bloom,
Shortcodes added, dispelling the gloom.
Buttons now flex, with text they can share,
User flows smoother, with logic laid bare.
Error logs whisper, as redirections sing,
Goonj CRM thrives, with the joy changes bring! 🎉


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Outside diff range and nitpick comments (2)
wp-content/themes/goonj-crm/functions.php (2)

567-580: Simplify the activity processing loop for clarity.

The loop used to process activities can be optimized for better readability. Additionally, consider using null coalescing operators for clarity when retrieving $officeId.

Apply this diff as a suggestion:

foreach ($fetchUserActivities as $activity) {
-    $officeId = $activity['Office_Visit.Goonj_Processing_Center'] ?: $activity['Material_Contribution.Goonj_Office'];
+    $officeId = $activity['Office_Visit.Goonj_Processing_Center'] ?? $activity['Material_Contribution.Goonj_Office'];
    $activityType = $activity['activity_type_id:label'];

    if ($officeId && in_array($activityType, ['Office visit', 'Material Contribution'])) {
        $officeActivities[$officeId][] = $activityType;
        if ($activityType === 'Office visit') {
            $hasOfficeVisit = true;
        } elseif ($activityType === 'Material Contribution') {
            $hasMaterialContribution = true;
        }
    }
}

594-598: Use consistent URL generation with WordPress functions.

Instead of hardcoding URLs, use WordPress functions like home_url() or site_url() to generate paths. This ensures the URLs are correctly formed, especially if the site URL changes.

Apply this diff as a suggestion:

if ($hasOfficeVisit) {
-    $redirectPath = '/material-contribution/details/';
+    $redirectPath = home_url('/material-contribution/details/');
    $buttonText = __('Material Contribution', 'goonj-crm');
} elseif ($hasMaterialContribution) {
-    $redirectPath = '/processing-center/material-contribution/details/';
+    $redirectPath = home_url('/processing-center/material-contribution/details/');
    $buttonText = __('Office Visit', 'goonj-crm');
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between ff3be2c and c3bdc49.

📒 Files selected for processing (1)
  • wp-content/themes/goonj-crm/functions.php (3 hunks)
🧰 Additional context used
🔇 Additional comments (1)
wp-content/themes/goonj-crm/functions.php (1)

617-618: Ensure the shortcode is registered after the function definition.

The shortcode goonj_pu_activity_button is added using add_shortcode() on line 617~ immediately after the function definition. Ensure that this placement is intentional and that the function exists before the shortcode is registered to avoid unexpected behavior.

Comment on lines +430 to +438
function goonj_generate_button_html($buttonUrl, $buttonText) {
return sprintf(
'<div class="volunteer-button-container">
<a href="%s" class="wp-block-button__link has-white-color has-vivid-red-background-color has-text-color has-background has-link-color wp-element-button volunteer-button-link">
Wish to Volunteer?
%s
</a>
</div>',
esc_url($buttonUrl)
esc_url($buttonUrl),
esc_html($buttonText)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider separating HTML from PHP logic for better maintainability.

Embedding HTML directly within PHP functions can make the code harder to read and maintain. Consider using template files or output buffers to separate the presentation from the logic. This aligns with the single responsibility principle and improves code clarity.

Comment on lines +500 to +615
function goonj_pu_activity_button() {
$activityId = isset($_GET['activityId']) ? intval($_GET['activityId']) : 0;

if (empty($activityId)) {
\Civi::log()->warning('Activity ID is missing');
return;
}

try {
// Fetch activity details
$activity = \Civi\Api4\Activity::get(FALSE)
->addSelect('source_contact_id', 'Office_Visit.Goonj_Processing_Center', 'Material_Contribution.Goonj_Office', 'activity_type_id:label')
->addJoin('ActivityContact AS activity_contact', 'LEFT')
->addWhere('id', '=', $activityId)
->execute()
->first();

if (!$activity) {
\Civi::log()->info('No activities found for Activity ID:', ['activityId' => $activityId]);
return;
}

$individualId = $activity['source_contact_id'];
$activityTypeLabel = $activity['activity_type_id:label'];

// Mapping activity type labels to their respective Goonj Office fields
$officeMapping = [
'Material Contribution' => 'Material_Contribution.Goonj_Office',
'Office visit' => 'Office_Visit.Goonj_Processing_Center',
];

// Determine the Goonj Office ID based on the activity type
$goonjOfficeId = null; // Initialize to null
if (array_key_exists($activityTypeLabel, $officeMapping)) {
$goonjOfficeId = $activity[$officeMapping[$activityTypeLabel]] ?? null; // Retrieve the office ID if it exists
}

// Handle case if Goonj Office ID is still null
if (is_null($goonjOfficeId)) {
\Civi::log()->warning('Goonj Office ID is null for Activity ID:', ['activityId' => $activityId]);
// Additional handling if needed (e.g., return or throw an error)
}


// Define today's date range
$todayDate = date('Y-m-d');
$startOfDay = $todayDate . ' 00:00:00';
$endOfDay = $todayDate . ' 23:59:59';

// Fetch user's pu activities for the day
$fetchUserActivities = \Civi\Api4\Activity::get(FALSE)
->addSelect('Office_Visit.Goonj_Processing_Center', 'activity_type_id:label', 'Material_Contribution.Goonj_Office')
->addWhere('source_contact_id', '=', $individualId)
->addWhere('activity_type_id:label', 'IN', ['Office visit', 'Material Contribution'])
->addWhere('created_date', '>=', $startOfDay)
->addWhere('created_date', '<=', $endOfDay)
->execute();

\Civi::log()->info('fetchUserActivities', ['fetchUserActivities' => $fetchUserActivities]);

// Initialize flags for activity types and office activities array
$hasOfficeVisit = false;
$hasMaterialContribution = false;
$officeActivities = [];

// Single loop to process activities
foreach ($fetchUserActivities as $activity) {
$officeId = $activity['Office_Visit.Goonj_Processing_Center'] ?: $activity['Material_Contribution.Goonj_Office'];
$activityType = $activity['activity_type_id:label'];

// Track if both activity types exist and group by office ID
if ($officeId && in_array($activityType, ['Office visit', 'Material Contribution'])) {
$officeActivities[$officeId][] = $activityType;

if ($activityType === 'Office visit') {
$hasOfficeVisit = true;
} elseif ($activityType === 'Material Contribution') {
$hasMaterialContribution = true;
}
}
}

// Check if any office ID has both activity types
foreach ($officeActivities as $activityTypes) {
if (count(array_unique($activityTypes)) === 2) {
return; // Both activity types exist, no button needed
}
}

// Determine redirect path and button text
$redirectPath = '';
$buttonText = '';

if ($hasOfficeVisit) {
$redirectPath = '/material-contribution/details/';
$buttonText = 'Material Contribution';
} elseif ($hasMaterialContribution) {
$redirectPath = '/processing-center/material-contribution/details/';
$buttonText = 'Office Visit';
}

$redirectParams = [
'source_contact_id' => $individualId,
'Office_Visit.Goonj_Processing_Center' => $hasOfficeVisit ? $goonjOfficeId : null,
'Material_Contribution.Goonj_Office' => $hasMaterialContribution ? $goonjOfficeId : null,
];

// Build redirect URL with parameters
$redirectPathWithParams = $redirectPath . '#?' . http_build_query(array_filter($redirectParams));

return goonj_generate_button_html($redirectPathWithParams, $buttonText);
} catch (\Exception $e) {
\Civi::log()->error('Error in goonj_pu_activity_button: ' . $e->getMessage());
return;
}
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor goonj_pu_activity_button() to adhere to the single responsibility principle.

The goonj_pu_activity_button() function is lengthy and handles multiple concerns such as data fetching, business logic, and HTML generation. This violates the single responsibility principle. Consider breaking it down into smaller, reusable functions or classes to enhance readability and maintainability.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored this function

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nishant22029, great to hear that you've refactored the goonj_pu_activity_button() function! This will enhance the code's readability and maintainability.


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

Comment on lines +593 to +599
if ($hasOfficeVisit) {
$redirectPath = '/material-contribution/details/';
$buttonText = 'Material Contribution';
} elseif ($hasMaterialContribution) {
$redirectPath = '/processing-center/material-contribution/details/';
$buttonText = 'Office Visit';
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure consistency between redirect paths and button texts.

There's a potential mismatch in the redirect paths and button texts:

  • When $hasOfficeVisit is true, $redirectPath is set to /material-contribution/details/, and $buttonText is 'Material Contribution'.
  • When $hasMaterialContribution is true, $redirectPath is /processing-center/material-contribution/details/, and $buttonText is 'Office Visit'.

This seems inconsistent and might lead to user confusion or incorrect redirection.

Please verify and correct the redirect paths and button texts to match the intended actions.

Comment on lines +489 to +491
$buttonText = 'Wish to Volunteer?';

return goonj_generate_volunteer_button_html($redirectPathWithParams);
return goonj_generate_button_html($redirectPathWithParams, $buttonText);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use localization functions for translatable strings.

The button text 'Wish to Volunteer?' is hardcoded. To support internationalization and localization, wrap the string with WordPress localization functions like __() or _e().

Apply this diff to fix the issue:

-$buttonText = 'Wish to Volunteer?';
+$buttonText = __('Wish to Volunteer?', 'goonj-crm');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$buttonText = 'Wish to Volunteer?';
return goonj_generate_volunteer_button_html($redirectPathWithParams);
return goonj_generate_button_html($redirectPathWithParams, $buttonText);
$buttonText = __('Wish to Volunteer?', 'goonj-crm');
return goonj_generate_button_html($redirectPathWithParams, $buttonText);

Comment on lines +602 to +605
'source_contact_id' => $individualId,
'Office_Visit.Goonj_Processing_Center' => $hasOfficeVisit ? $goonjOfficeId : null,
'Material_Contribution.Goonj_Office' => $hasMaterialContribution ? $goonjOfficeId : null,
];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid including null values in query parameters.

In the $redirectParams array, null values for parameters like 'Office_Visit.Goonj_Processing_Center' or 'Material_Contribution.Goonj_Office' may result in query strings with keys but no values, which can cause confusion in URL parsing.

Filter out null values before building the query string:

$redirectParams = [
    'source_contact_id' => $individualId,
    'Office_Visit.Goonj_Processing_Center' => $hasOfficeVisit ? $goonjOfficeId : null,
    'Material_Contribution.Goonj_Office' => $hasMaterialContribution ? $goonjOfficeId : null,
];

+ $redirectParams = array_filter($redirectParams, function($value) {
+     return !is_null($value);
+ });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'source_contact_id' => $individualId,
'Office_Visit.Goonj_Processing_Center' => $hasOfficeVisit ? $goonjOfficeId : null,
'Material_Contribution.Goonj_Office' => $hasMaterialContribution ? $goonjOfficeId : null,
];
'source_contact_id' => $individualId,
'Office_Visit.Goonj_Processing_Center' => $hasOfficeVisit ? $goonjOfficeId : null,
'Material_Contribution.Goonj_Office' => $hasMaterialContribution ? $goonjOfficeId : null,
];
$redirectParams = array_filter($redirectParams, function($value) {
return !is_null($value);
});

Comment on lines +538 to +541
if (is_null($goonjOfficeId)) {
\Civi::log()->warning('Goonj Office ID is null for Activity ID:', ['activityId' => $activityId]);
// Additional handling if needed (e.g., return or throw an error)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle null $goonjOfficeId to prevent potential errors.

When $goonjOfficeId is null, a warning is logged, but the execution continues, which might lead to issues when $goonjOfficeId is used later in the code. It's important to handle this scenario explicitly to prevent unexpected behavior.

Apply this diff to fix the issue:

if (is_null($goonjOfficeId)) {
    \Civi::log()->warning('Goonj Office ID is null for Activity ID:', ['activityId' => $activityId]);
+   return; // Exit early since office ID is essential for further processing
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (is_null($goonjOfficeId)) {
\Civi::log()->warning('Goonj Office ID is null for Activity ID:', ['activityId' => $activityId]);
// Additional handling if needed (e.g., return or throw an error)
}
if (is_null($goonjOfficeId)) {
\Civi::log()->warning('Goonj Office ID is null for Activity ID:', ['activityId' => $activityId]);
return; // Exit early since office ID is essential for further processing
}

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (2)
wp-content/themes/goonj-crm/templates/button-template.php (2)

3-4: Good use of escaping functions. Consider adding default values.

The use of esc_url() and esc_html() for escaping the URL and button text is excellent for security. However, to improve robustness, consider adding default values for $buttonUrl and $buttonText.

Here's a suggestion to add default values:

-    <a href="<?php echo esc_url($buttonUrl); ?>" class="wp-block-button__link has-white-color has-vivid-red-background-color has-text-color has-background has-link-color wp-element-button volunteer-button-link">
-        <?php echo esc_html($buttonText); ?>
+    <a href="<?php echo esc_url($buttonUrl ?? '#'); ?>" class="wp-block-button__link has-white-color has-vivid-red-background-color has-text-color has-background has-link-color wp-element-button volunteer-button-link">
+        <?php echo esc_html($buttonText ?? 'Click here'); ?>

3-3: Good use of CSS classes. Consider using a constant for class list.

The use of multiple CSS classes provides flexibility in styling. However, to improve maintainability, consider defining the class list as a constant or variable.

Here's a suggestion to improve maintainability:

<?php
$button_classes = 'wp-block-button__link has-white-color has-vivid-red-background-color has-text-color has-background has-link-color wp-element-button volunteer-button-link';
?>
<a href="<?php echo esc_url($buttonUrl ?? '#'); ?>" class="<?php echo esc_attr($button_classes); ?>">
    <?php echo esc_html($buttonText ?? 'Click here'); ?>
</a>

This approach allows for easier modification of the class list in the future.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between c3bdc49 and 106c064.

📒 Files selected for processing (2)
  • wp-content/themes/goonj-crm/functions.php (3 hunks)
  • wp-content/themes/goonj-crm/templates/button-template.php (1 hunks)
🧰 Additional context used
🔇 Additional comments (4)
wp-content/themes/goonj-crm/templates/button-template.php (2)

1-1: LGTM! Clear file identification.

The comment at the beginning of the file clearly identifies its purpose as a button template. This is a good practice for maintaining code readability and organization.


2-6: LGTM! Semantic HTML structure.

The HTML structure is clean and semantic. Using a <div> as a container and an <a> element as a button is appropriate, especially if the button leads to a new page or action.

wp-content/themes/goonj-crm/functions.php (2)

486-486: Use Localization Functions for Translatable Strings

The button text 'Wish to Volunteer?' is hardcoded. To support internationalization and localization, wrap the string with WordPress localization functions like __() or _e().


497-541: Good Refactoring to Support Single Responsibility Principle

The goonj_pu_activity_button function has been refactored to delegate tasks to smaller functions like fetch_activity_details and generate_activity_button. This improves code readability and aligns with the single responsibility principle.

Comment on lines +603 to +639
function generate_activity_button($officeActivities, $goonjOfficeId, $individualId) {
$hasOfficeVisit = false;
$hasMaterialContribution = false;

// Check for office visit and material contribution activity types
foreach ($officeActivities as $activityTypes) {
if (in_array('Office visit', $activityTypes)) {
$hasOfficeVisit = true;
}
if (in_array('Material Contribution', $activityTypes)) {
$hasMaterialContribution = true;
}
}

$redirectPath = '';
$buttonText = '';

// Set the correct redirect path and button text based on activities completed
if ($hasOfficeVisit) {
$redirectPath = '/processing-center/material-contribution/details/';
$buttonText = 'Proceed to Material Contribution';
} elseif ($hasMaterialContribution) {
$redirectPath = '/processing-center/office-visit/details/';
$buttonText = 'Proceed to Office Visit';
}

$redirectParams = [
'source_contact_id' => $individualId,
'Material_Contribution.Goonj_Office' => $hasOfficeVisit ? $goonjOfficeId : null,
'Office_Visit.Goonj_Processing_Center' => $hasMaterialContribution ? $goonjOfficeId : null,
];

// Create the full URL with query parameters
$redirectPathWithParams = $redirectPath . '#?' . http_build_query(array_filter($redirectParams));

return goonj_generate_button_html($redirectPathWithParams, $buttonText);
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor to Eliminate Duplicate Code and Simplify Logic

The generate_activity_button function contains duplicate code and complex conditional logic. Refactoring can improve readability and adherence to the DRY (Don't Repeat Yourself) principle.

Consider this refactored version:

function generate_activity_button($officeActivities, $goonjOfficeId, $individualId) {
    $activityTypes = ['Office visit', 'Material Contribution'];
    $completedActivities = $officeActivities[$goonjOfficeId] ?? [];
    $pendingActivities = array_diff($activityTypes, $completedActivities);

    if (empty($pendingActivities)) {
        return; // All activities completed
    }

    $activityMap = [
        'Office visit' => [
            'redirectPath' => '/processing-center/office-visit/details/',
            'buttonText' => __('Proceed to Office Visit', 'goonj-crm'),
            'queryParam' => 'Office_Visit.Goonj_Processing_Center',
        ],
        'Material Contribution' => [
            'redirectPath' => '/processing-center/material-contribution/details/',
            'buttonText' => __('Proceed to Material Contribution', 'goonj-crm'),
            'queryParam' => 'Material_Contribution.Goonj_Office',
        ],
    ];

    $nextActivity = reset($pendingActivities);
    $details = $activityMap[$nextActivity];

    $redirectParams = [
        'source_contact_id' => $individualId,
        $details['queryParam'] => $goonjOfficeId,
    ];

    $redirectPathWithParams = $details['redirectPath'] . '#?' . http_build_query($redirectParams);

    return goonj_generate_button_html($redirectPathWithParams, $details['buttonText']);
}

This refactoring reduces duplication and simplifies the logic by using arrays and built-in functions.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done changes

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nishant22029, great job updating the code! The refactored function looks much cleaner and more maintainable now. Thank you for making the changes.

(^・ω・^ )


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

Comment on lines +621 to +627
if ($hasOfficeVisit) {
$redirectPath = '/processing-center/material-contribution/details/';
$buttonText = 'Proceed to Material Contribution';
} elseif ($hasMaterialContribution) {
$redirectPath = '/processing-center/office-visit/details/';
$buttonText = 'Proceed to Office Visit';
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use Localization Functions for Button Texts

The button texts 'Proceed to Material Contribution' and 'Proceed to Office Visit' are hardcoded. For better localization support, wrap these strings with WordPress localization functions.

Apply this diff to fix the issue:

 if ($hasOfficeVisit) {
     $redirectPath = '/processing-center/material-contribution/details/';
-    $buttonText = 'Proceed to Material Contribution';
+    $buttonText = __('Proceed to Material Contribution', 'goonj-crm');
 } elseif ($hasMaterialContribution) {
     $redirectPath = '/processing-center/office-visit/details/';
-    $buttonText = 'Proceed to Office Visit';
+    $buttonText = __('Proceed to Office Visit', 'goonj-crm');
 }

This change ensures that the button texts can be translated into other languages.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ($hasOfficeVisit) {
$redirectPath = '/processing-center/material-contribution/details/';
$buttonText = 'Proceed to Material Contribution';
} elseif ($hasMaterialContribution) {
$redirectPath = '/processing-center/office-visit/details/';
$buttonText = 'Proceed to Office Visit';
}
if ($hasOfficeVisit) {
$redirectPath = '/processing-center/material-contribution/details/';
$buttonText = __('Proceed to Material Contribution', 'goonj-crm');
} elseif ($hasMaterialContribution) {
$redirectPath = '/processing-center/office-visit/details/';
$buttonText = __('Proceed to Office Visit', 'goonj-crm');
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done change

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nishant22029 Great! Thank you for making the change.

(_/)
( ^.^ )
(")__(")


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

Comment on lines +430 to +436
function goonj_generate_button_html($buttonUrl, $buttonText) {
set_query_var('buttonUrl', $buttonUrl);
set_query_var('buttonText', $buttonText);

ob_start();
get_template_part('templates/button-template');
return ob_get_clean();
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid Using Global State: Pass Variables Directly to Templates

The goonj_generate_button_html function relies on set_query_var to pass variables to the template, which modifies global state and can lead to unintended side effects. Instead, pass variables directly to the template to enhance code clarity and maintainability.

Apply this diff to refactor the function:

 function goonj_generate_button_html($buttonUrl, $buttonText) {
-    set_query_var('buttonUrl', $buttonUrl);
-    set_query_var('buttonText', $buttonText);
-
     ob_start();
-    get_template_part('templates/button-template');
+    get_template_part('templates/button-template', null, [
+        'buttonUrl' => $buttonUrl,
+        'buttonText' => $buttonText,
+    ]);
     return ob_get_clean();
 }

This approach avoids modifying global variables and makes the data flow more explicit.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function goonj_generate_button_html($buttonUrl, $buttonText) {
set_query_var('buttonUrl', $buttonUrl);
set_query_var('buttonText', $buttonText);
ob_start();
get_template_part('templates/button-template');
return ob_get_clean();
function goonj_generate_button_html($buttonUrl, $buttonText) {
ob_start();
get_template_part('templates/button-template', null, [
'buttonUrl' => $buttonUrl,
'buttonText' => $buttonText,
]);
return ob_get_clean();
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. change

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nishant22029 Thank you for implementing the change!

(^o^/)


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 106c064 and f9358d8.

📒 Files selected for processing (1)
  • wp-content/themes/goonj-crm/functions.php (3 hunks)
🧰 Additional context used

'Individual1' => $individualId,
'message' => 'individual-user'
]);
$buttonText = 'Wish to Volunteer?';
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use localization functions for translatable strings.

The button text 'Wish to Volunteer?' is hardcoded. For internationalization support, wrap the string with WordPress localization functions like __().

Apply this diff to make the string translatable:

 $redirectPathWithParams = $redirectPath . '#?' . http_build_query([
     'Individual1' => $individualId,
     'message' => 'individual-user'
 ]);
-$buttonText = 'Wish to Volunteer?';
+$buttonText = __('Wish to Volunteer?', 'goonj-crm');

This change enables the string to be translated into other languages.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$buttonText = 'Wish to Volunteer?';
$buttonText = __('Wish to Volunteer?', 'goonj-crm');

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changes done

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nishant22029, thank you for making the changes! 🎉

   (\_/)
   (•_•)
  / >🎉

If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

$details['queryParam'] => $goonjOfficeId,
];

$redirectPathWithParams = $details['redirectPath'] . '#?' . http_build_query($redirectParams);
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct URL generation by placing query parameters before the fragment identifier.

Appending query parameters after # can prevent them from being sent to the server, as they are treated as fragment identifiers. Adjust the URL construction to include query parameters before the # symbol.

Apply this diff to fix the URL construction:

-	$redirectPathWithParams = $details['redirectPath'] . '#?' . http_build_query($redirectParams);
+	$redirectPathWithParams = $details['redirectPath'] . '?' . http_build_query($redirectParams);

This ensures that query parameters are properly recognized and processed by the server.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$redirectPathWithParams = $details['redirectPath'] . '#?' . http_build_query($redirectParams);
$redirectPathWithParams = $details['redirectPath'] . '?' . http_build_query($redirectParams);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changes done

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nishant22029, great! Thank you for making the changes.

(^•^)/


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

add_shortcode('goonj_contribution_volunteer_signup_button', 'goonj_contribution_volunteer_signup_button');

function goonj_pu_activity_button() {
$activityId = isset($_GET['activityId']) ? intval($_GET['activityId']) : 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance input validation using filter_input() for activityId.

Currently, activityId is retrieved directly from $_GET and cast using intval(). For better security and input validation, use filter_input() with FILTER_VALIDATE_INT to safely retrieve and validate the activityId parameter.

Apply this diff to improve input sanitization:

-function goonj_pu_activity_button() {
-	$activityId = isset($_GET['activityId']) ? intval($_GET['activityId']) : 0;
+function goonj_pu_activity_button() {
+	$activityId = filter_input(INPUT_GET, 'activityId', FILTER_VALIDATE_INT);
+	if ($activityId === false || $activityId === null) {
+		$activityId = 0;
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$activityId = isset($_GET['activityId']) ? intval($_GET['activityId']) : 0;
function goonj_pu_activity_button() {
$activityId = filter_input(INPUT_GET, 'activityId', FILTER_VALIDATE_INT);
if ($activityId === false || $activityId === null) {
$activityId = 0;
}

Comment on lines +568 to +581
function fetch_contact_activities_for_today($individualId) {
$todayDate = date('Y-m-d');
$startOfDay = $todayDate . ' 00:00:00';
$endOfDay = $todayDate . ' 23:59:59';

return \Civi\Api4\Activity::get(FALSE)
->addSelect('Office_Visit.Goonj_Processing_Center', 'activity_type_id:label', 'Material_Contribution.Goonj_Office')
->addWhere('source_contact_id', '=', $individualId)
->addWhere('activity_type_id:label', 'IN', ['Office visit', 'Material Contribution'])
->addWhere('created_date', '>=', $startOfDay)
->addWhere('created_date', '<=', $endOfDay)
->execute();
}

This comment was marked as off-topic.

@nishant22029 nishant22029 requested a review from pokhiii October 10, 2024 11:22
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (1)
wp-content/themes/goonj-crm/functions.php (1)

430-437: Add PHPDoc comments to goonj_generate_button_html for better readability.

Including PHPDoc comments will improve code maintainability by providing clear documentation of the function's purpose, parameters, and return values.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 1203d63 and 95018a0.

📒 Files selected for processing (1)
  • wp-content/themes/goonj-crm/functions.php (3 hunks)
🧰 Additional context used

Comment on lines +606 to +639
function goonj_generate_activity_button($officeActivities, $goonjOfficeId, $individualId) {
$activityTypes = ['Office visit', 'Material Contribution'];
$completedActivities = $officeActivities[$goonjOfficeId] ?? [];
$pendingActivities = array_diff($activityTypes, $completedActivities);

if (empty($pendingActivities)) {
return; // All activities completed
}

$activityMap = [
'Office visit' => [
'redirectPath' => '/processing-center/office-visit/details/',
'buttonText' => __('Proceed to Office Visit', 'goonj-crm'),
'queryParam' => 'Office_Visit.Goonj_Processing_Center',
],
'Material Contribution' => [
'redirectPath' => '/processing-center/material-contribution/details/',
'buttonText' => __('Proceed to Material Contribution', 'goonj-crm'),
'queryParam' => 'Material_Contribution.Goonj_Office',
],
];

$nextActivity = reset($pendingActivities);
$details = $activityMap[$nextActivity];

$redirectParams = [
'source_contact_id' => $individualId,
$details['queryParam'] => $goonjOfficeId,
];

$redirectPathWithParams = $details['redirectPath'] . '#?' . http_build_query($redirectParams);

return goonj_generate_button_html($redirectPathWithParams, $details['buttonText']);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor URL construction into a reusable function to eliminate duplication.

The logic for building $redirectPathWithParams is repeated in different parts of the codebase. Extracting this into a utility function promotes code reuse and adheres to the DRY (Don't Repeat Yourself) principle.

Example utility function:

function build_redirect_url($basePath, $params) {
    return $basePath . '?' . http_build_query($params);
}

Update the code to use the utility function:

-$redirectPathWithParams = $details['redirectPath'] . '?'
-    . http_build_query($redirectParams);
+$redirectPathWithParams = build_redirect_url(
+    $details['redirectPath'],
+    $redirectParams
+);

Comment on lines +566 to +571
function fetch_contact_pu_activities_for_today($individualId) {

$timezone = new \DateTimeZone('UTC');
$today = new \DateTime('now', $timezone);
$startOfDay = $today->setTime(0, 0)->format('Y-m-d H:i:s');
$endOfDay = $today->setTime(23, 59, 59)->format('Y-m-d H:i:s');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Clone the DateTime object to prevent mutability issues.

In fetch_contact_pu_activities_for_today(), modifying the same DateTime object for $startOfDay and $endOfDay can lead to unexpected results due to object mutability. Cloning the object before setting the time ensures accurate date ranges.

Apply this diff to fix the issue:

 $timezone = new \DateTimeZone('UTC');
 $today = new \DateTime('now', $timezone);
-$startOfDay = $today->setTime(0, 0)->format('Y-m-d H:i:s');
-$endOfDay = $today->setTime(23, 59, 59)->format('Y-m-d H:i:s');
+$startOfDay = (clone $today)->setTime(0, 0)->format('Y-m-d H:i:s');
+$endOfDay = (clone $today)->setTime(23, 59, 59)->format('Y-m-d H:i:s');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function fetch_contact_pu_activities_for_today($individualId) {
$timezone = new \DateTimeZone('UTC');
$today = new \DateTime('now', $timezone);
$startOfDay = $today->setTime(0, 0)->format('Y-m-d H:i:s');
$endOfDay = $today->setTime(23, 59, 59)->format('Y-m-d H:i:s');
function fetch_contact_pu_activities_for_today($individualId) {
$timezone = new \DateTimeZone('UTC');
$today = new \DateTime('now', $timezone);
$startOfDay = (clone $today)->setTime(0, 0)->format('Y-m-d H:i:s');
$endOfDay = (clone $today)->setTime(23, 59, 59)->format('Y-m-d H:i:s');

@nishant22029 nishant22029 changed the title Add shortcode for pu activites Add Dynamic Button for PU Activities using shortcode: Proceed to Office Visit or Material Contribution Oct 11, 2024
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (1)
wp-content/themes/goonj-crm/functions.php (1)

617-618: Use defined constants for activity types instead of magic strings

In the $activityTypes array, activity type labels are hard-coded as 'Office visit' and 'Material Contribution'. Defining these strings as constants can improve code readability and maintainability, reducing the risk of typos or inconsistencies.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 95018a0 and 4482f05.

📒 Files selected for processing (1)
  • wp-content/themes/goonj-crm/functions.php (3 hunks)
🧰 Additional context used
🔇 Additional comments (1)
wp-content/themes/goonj-crm/functions.php (1)

430-437: Function goonj_generate_button_html() is well-structured

The goonj_generate_button_html() function appropriately abstracts the button generation logic, enhancing code reusability and maintainability.

Comment on lines +499 to +551
// Function to determine which activity button (if any) should be displayed for the user
function goonj_pu_activity_button() {
// Retrieve the activity ID from the URL parameters (GET request)
$activityId = isset($_GET['activityId']) ? intval($_GET['activityId']) : 0;

// If no valid activity ID is provided, log an error and exit the function
if (empty($activityId)) {
\Civi::log()->info('Activity ID is missing', ['activityId' => $activityId]);
return;
}

try {
// Fetch the details of the current activity (either Office Visit or Material Contribution)
$activity = goonj_fetch_pu_activity_details($activityId);

// If no activity details are found, log an error and exit the function
if (!$activity) {
\Civi::log()->info('No activities found for Activity ID:', ['activityId' => $activityId]);
return;
}

// Extract the individual's ID from the activity data
$individualId = $activity['source_contact_id'];

// Fetch the Goonj office ID from the activity details (can be from Office Visit or Material Contribution)
$goonjOfficeId = goonj_get_goonj_office_id($activity);

// If the office ID is null, log an error and exit the function
if (is_null($goonjOfficeId)) {
\Civi::log()->info('Goonj Office ID is null for Activity ID:', ['activityId' => $activityId]);
return;
}

// Fetch all activities for this individual that occurred today (Office Visits and Material Contributions)
$contactActivities = fetch_contact_pu_activities_for_today($individualId);

// Process these activities to determine what has already been completed at this office
$officeActivities = goonj_process_pu_activities($contactActivities);

// If both Office Visit and Material Contribution have been completed at the same office, exit the function
if (goonj_check_if_both_pu_activity_types_exist($officeActivities)) {
return; // Both activities are done, no button needed
}

// Generate the appropriate button for the next pending activity and return the HTML
return goonj_generate_activity_button($officeActivities, $goonjOfficeId, $individualId);

} catch (\Exception $e) {
// Log any exceptions that occur during the process
\Civi::log()->error('Error in goonj_pu_activity_button: ' . $e->getMessage());
return;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor goonj_pu_activity_button() to adhere to the single responsibility principle

The goonj_pu_activity_button() function handles multiple responsibilities, including input validation, data retrieval, business logic, and output generation. This violates the single responsibility principle and can make the code harder to read and maintain. Consider breaking it down into smaller, focused functions that each handle a specific task.

Comment on lines +631 to +643
// Define the mapping for each activity type to its respective redirect path, button text, and query parameter
$activityMap = [
'Office visit' => [
'redirectPath' => '/processing-center/office-visit/details/',
'buttonText' => __('Proceed to Office Visit', 'goonj-crm'),
'queryParam' => 'Office_Visit.Goonj_Processing_Center',
],
'Material Contribution' => [
'redirectPath' => '/processing-center/material-contribution/details/',
'buttonText' => __('Proceed to Material Contribution', 'goonj-crm'),
'queryParam' => 'Material_Contribution.Goonj_Office',
],
];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Extract activity mapping to configuration or class constants

The $activityMap array in goonj_generate_activity_button() is hard-coded within the function. To enhance maintainability and facilitate future updates, consider extracting this mapping to a configuration file or defining it as a class constant. This approach centralizes the activity mapping data, making it easier to manage and reducing duplication.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (3)
wp-content/themes/goonj-crm/templates/primary-button.php (1)

1-5: Good structure, but let's enhance accessibility!

The markup looks clean and follows WordPress coding standards. Great job using esc_url() and esc_html() for security! However, we can make this button even better:

  1. Add role="button" to the anchor tag to explicitly define its purpose.
  2. Consider adding an aria-label with a more descriptive text about the button's action.

Here's a suggested improvement:

 <div class="volunteer-button-container">
-	<a href="<?php echo esc_url( $args['button_url'] ); ?>" class="wp-block-button__link has-white-color has-vivid-red-background-color has-text-color has-background has-link-color wp-element-button volunteer-button-link">
+	<a href="<?php echo esc_url( $args['button_url'] ); ?>" class="wp-block-button__link has-white-color has-vivid-red-background-color has-text-color has-background has-link-color wp-element-button volunteer-button-link" role="button" aria-label="<?php echo esc_attr( $args['button_aria_label'] ?? $args['button_text'] ); ?>">
 		<?php echo esc_html( $args['button_text'] ); ?>
 	</a>
 </div>

This change improves accessibility while maintaining the existing functionality. The aria-label fallback to button_text ensures compatibility if the new attribute isn't provided.

wp-content/themes/goonj-crm/functions.php (2)

Line range hint 691-755: Simplify logic and improve error handling in goonj_redirect_after_individual_creation.

The function's complexity could be reduced, and its error handling improved:

  1. Consider using a switch statement or a mapping object for different creation flows to simplify the logic.
  2. Implement more robust error handling and logging, especially for edge cases.
  3. Extract the redirection URL generation into separate functions for each flow type.

Here's a suggestion for simplifying the logic:

function goonj_redirect_after_individual_creation() {
    if (!isset($_GET['goonjAction']) || $_GET['goonjAction'] !== 'individualCreated' || !isset($_GET['individualId'])) {
        return;
    }

    $individual = goonj_fetch_individual_details($_GET['individualId']);
    if (!$individual) {
        \Civi::log()->error('Failed to fetch individual details', ['individualId' => $_GET['individualId']]);
        return;
    }

    $redirectPath = goonj_get_redirect_path($individual);
    if ($redirectPath) {
        wp_safe_redirect($redirectPath);
        exit;
    }
}

function goonj_get_redirect_path($individual) {
    $flowHandlers = [
        'material-contribution' => 'goonj_handle_material_contribution_flow',
        'office-visit' => 'goonj_handle_office_visit_flow',
        'office-visit-contribution' => 'goonj_handle_office_visit_contribution_flow',
    ];

    $creationFlow = $individual['Individual_fields.Creation_Flow'];
    if (isset($flowHandlers[$creationFlow])) {
        return $flowHandlers[$creationFlow]($individual);
    }

    \Civi::log()->warning('Unknown creation flow', ['flow' => $creationFlow, 'individualId' => $individual['id']]);
    return '';
}

// Implement individual handler functions for each flow type

This approach would make the function more maintainable and easier to extend in the future.


Line range hint 1-755: Overall code structure and maintainability improvements needed.

While the code is functional, there are several areas where it could be improved to enhance maintainability, readability, and adherence to best practices:

  1. Separation of Concerns: Many functions are handling multiple responsibilities. Consider breaking these down into smaller, more focused functions or classes.

  2. Error Handling: Implement a more consistent and robust error handling strategy throughout the file. This could include custom exceptions and more detailed logging.

  3. Configuration Management: Move hardcoded values and mappings to configuration files or database tables for easier management and updates.

  4. Code Organization: Group related functions together and consider moving some functionality into separate files or classes for better organization.

  5. Dependency Injection: Implement dependency injection to improve testability and reduce tight coupling between components.

  6. Documentation: Add more inline documentation, especially for complex logic and CiviCRM API interactions.

  7. Input Validation: Enhance input validation and sanitization, particularly for user inputs and GET/POST parameters.

Consider implementing a service layer to encapsulate CiviCRM API calls and business logic. This would help separate the WordPress-specific code from the CiviCRM integration logic, making the codebase more modular and easier to maintain.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 4482f05 and bb2f361.

📒 Files selected for processing (2)
  • wp-content/themes/goonj-crm/functions.php (5 hunks)
  • wp-content/themes/goonj-crm/templates/primary-button.php (1 hunks)
🧰 Additional context used

Comment on lines +506 to +557
function goonj_pu_activity_button() {
// Retrieve the activity ID from the URL parameters (GET request)
$activityId = isset( $_GET['activityId'] ) ? intval( $_GET['activityId'] ) : 0;

// If no valid activity ID is provided, log an error and exit the function
if ( empty( $activityId ) ) {
\Civi::log()->info( 'Activity ID is missing', array( 'activityId' => $activityId ) );
return;
}

try {
// Fetch the details of the current activity (either Office Visit or Material Contribution)
$activity = goonj_fetch_pu_activity_details( $activityId );

// If no activity details are found, log an error and exit the function
if ( ! $activity ) {
\Civi::log()->info( 'No activities found for Activity ID:', array( 'activityId' => $activityId ) );
return;
}

// Extract the individual's ID from the activity data
$individualId = $activity['source_contact_id'];

// Fetch the Goonj office ID from the activity details (can be from Office Visit or Material Contribution)
$goonjOfficeId = goonj_get_goonj_office_id( $activity );

// If the office ID is null, log an error and exit the function
if ( is_null( $goonjOfficeId ) ) {
\Civi::log()->info( 'Goonj Office ID is null for Activity ID:', array( 'activityId' => $activityId ) );
return;
}

$contactSubTypes = $contact['contact_sub_type'] ?? [];
// Fetch all activities for this individual that occurred today (Office Visits and Material Contributions)
$contactActivities = fetch_contact_pu_activities_for_today( $individualId );

// Process these activities to determine what has already been completed at this office
$officeActivities = goonj_process_pu_activities( $contactActivities );

// If both Office Visit and Material Contribution have been completed at the same office, exit the function
if ( goonj_check_if_both_pu_activity_types_exist( $officeActivities ) ) {
return; // Both activities are done, no button needed
}

// Generate the appropriate button for the next pending activity and return the HTML
return goonj_generate_activity_button( $officeActivities, $goonjOfficeId, $individualId );

} catch ( \Exception $e ) {
// Log any exceptions that occur during the process
\Civi::log()->error( 'Error in goonj_pu_activity_button: ' . $e->getMessage() );
return;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance modularity and error handling in goonj_pu_activity_button.

While the function is well-structured, it's handling multiple responsibilities and could benefit from further modularization. Consider the following improvements:

  1. Extract the activity fetching and processing logic into separate service methods.
  2. Implement a more consistent error handling strategy, possibly using custom exceptions.
  3. Consider using a factory pattern for button generation to improve extensibility.

Here's a high-level suggestion for refactoring:

function goonj_pu_activity_button() {
    try {
        $activityService = new ActivityService();
        $buttonFactory = new ButtonFactory();

        $activityId = $activityService->getActivityIdFromRequest();
        $activity = $activityService->fetchActivityDetails($activityId);
        $officeActivities = $activityService->getOfficeActivities($activity);

        return $buttonFactory->createButton($officeActivities);
    } catch (ActivityException $e) {
        \Civi::log()->error('Error in goonj_pu_activity_button: ' . $e->getMessage());
        return null;
    }
}

This approach would improve separation of concerns and make the code more maintainable.

Comment on lines +621 to +667
function goonj_generate_activity_button( $officeActivities, $goonjOfficeId, $individualId ) {
// Define the two activity types: Office visit and Material Contribution
$activityTypes = array( 'Office visit', 'Material Contribution' );

// Get the list of completed activities for the specified office
// If no activities are found for this office, an empty array is returned
$completedActivities = $officeActivities[ $goonjOfficeId ] ?? array();

// Find the activities that are still pending by comparing with the list of activity types
$pendingActivities = array_diff( $activityTypes, $completedActivities );

// If all activities have been completed, no button is needed, so exit the function
if ( empty( $pendingActivities ) ) {
return;
}

// Define the mapping for each activity type to its respective redirect path, button text, and query parameter
$activityMap = array(
'Office visit' => array(
'redirectPath' => '/processing-center/office-visit/details/',
'buttonText' => __( 'Proceed to Office Visit', 'goonj-crm' ),
'queryParam' => 'Office_Visit.Goonj_Processing_Center',
),
'Material Contribution' => array(
'redirectPath' => '/processing-center/material-contribution/details/',
'buttonText' => __( 'Proceed to Material Contribution', 'goonj-crm' ),
'queryParam' => 'Material_Contribution.Goonj_Office',
),
);

// Get the first pending activity from the list of pending activities
$nextActivity = reset( $pendingActivities );
// Fetch the relevant details for the next activity, including the redirect path and button text
$details = $activityMap[ $nextActivity ];

// Prepare the query parameters needed for the redirection, including the individual and office IDs
$redirectParams = array(
'source_contact_id' => $individualId,
$details['queryParam'] => $goonjOfficeId,
);

// Generate the full redirect URL with the necessary query parameters
$redirectPathWithParams = $details['redirectPath'] . '#?' . http_build_query( $redirectParams );

// Generate the HTML for the button using the redirect URL and the button text
return goonj_generate_button_html( $redirectPathWithParams, $details['buttonText'] );
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance configurability of goonj_generate_activity_button.

The function is well-structured, but it could benefit from increased flexibility:

  1. Move the $activityMap to a configuration file or database to allow easier updates without code changes.
  2. Consider using dependency injection for the button generation to improve testability.

Here's a suggestion for externalizing the activity map:

function goonj_get_activity_map() {
    // This could be loaded from a config file or database
    return [
        'Office visit' => [
            'redirectPath' => '/processing-center/office-visit/details/',
            'buttonText' => __('Proceed to Office Visit', 'goonj-crm'),
            'queryParam' => 'Office_Visit.Goonj_Processing_Center',
        ],
        'Material Contribution' => [
            'redirectPath' => '/processing-center/material-contribution/details/',
            'buttonText' => __('Proceed to Material Contribution', 'goonj-crm'),
            'queryParam' => 'Material_Contribution.Goonj_Office',
        ],
    ];
}

function goonj_generate_activity_button($officeActivities, $goonjOfficeId, $individualId) {
    $activityMap = goonj_get_activity_map();
    // Rest of the function remains the same
}

This change would make it easier to manage activity types and their associated details.

Comment on lines +444 to +501

function goonj_contribution_volunteer_signup_button() {
$activityId = isset($_GET['activityId']) ? intval($_GET['activityId']) : 0;

if (empty($activityId)) {
\Civi::log()->warning('Activity ID is missing');
return;
}

try {
$activities = \Civi\Api4\Activity::get(FALSE)
->addSelect('source_contact_id')
->addJoin('ActivityContact AS activity_contact', 'LEFT')
->addWhere('id', '=', $activityId)
->addWhere('activity_type_id:label', '=', 'Material Contribution')
->execute();

if ($activities->count() === 0) {
\Civi::log()->info('No activities found for Activity ID:', ['activityId' => $activityId]);
return;
}

$activity = $activities->first();
$individualId = $activity['source_contact_id'];

$contact = \Civi\Api4\Contact::get(FALSE)
->addSelect('contact_sub_type')
->addWhere('id', '=', $individualId)
->execute()
->first();

if (empty($contact)) {
\Civi::log()->info('Contact not found', ['contact' => $contact['id']]);
$activityId = isset( $_GET['activityId'] ) ? intval( $_GET['activityId'] ) : 0;

if ( empty( $activityId ) ) {
\Civi::log()->warning( 'Activity ID is missing' );
return;
}

try {
$activities = \Civi\Api4\Activity::get( false )
->addSelect( 'source_contact_id' )
->addJoin( 'ActivityContact AS activity_contact', 'LEFT' )
->addWhere( 'id', '=', $activityId )
->addWhere( 'activity_type_id:label', '=', 'Material Contribution' )
->execute();

if ( $activities->count() === 0 ) {
\Civi::log()->info( 'No activities found for Activity ID:', array( 'activityId' => $activityId ) );
return;
}

$activity = $activities->first();
$individualId = $activity['source_contact_id'];

$contact = \Civi\Api4\Contact::get( false )
->addSelect( 'contact_sub_type' )
->addWhere( 'id', '=', $individualId )
->execute()
->first();

if ( empty( $contact ) ) {
\Civi::log()->info( 'Contact not found', array( 'contact' => $contact['id'] ) );
return;
}

$contactSubTypes = $contact['contact_sub_type'] ?? array();

// If the individual is already a volunteer, don't show the button
if ( in_array( 'Volunteer', $contactSubTypes ) ) {
return;
}

$redirectPath = '/volunteer-registration/form-with-details/';
$redirectPathWithParams = $redirectPath . '#?' . http_build_query(
array(
'Individual1' => $individualId,
'message' => 'individual-user',
)
);
$buttonText = __( 'Wish to Volunteer?', 'goonj-crm' );

return goonj_generate_button_html( $redirectPathWithParams, $buttonText );
} catch ( \Exception $e ) {
\Civi::log()->error( 'Error in goonj_contribution_volunteer_signup_button: ' . $e->getMessage() );
return;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Refactor to improve separation of concerns and enhance security.

The goonj_contribution_volunteer_signup_button function is handling multiple responsibilities, which violates the Single Responsibility Principle. Consider breaking it down into smaller, focused functions. Additionally, there's a potential security risk with how the activity ID is handled.

  1. Separate the data retrieval, business logic, and HTML generation into distinct functions.
  2. Use filter_input() for safer input handling of the activity ID.
  3. Consider moving the CiviCRM API calls to a separate service class for better organization.

Here's a suggested refactor for the activity ID retrieval:

-$activityId = isset($_GET['activityId']) ? intval($_GET['activityId']) : 0;
+$activityId = filter_input(INPUT_GET, 'activityId', FILTER_VALIDATE_INT) ?: 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function goonj_contribution_volunteer_signup_button() {
$activityId = isset($_GET['activityId']) ? intval($_GET['activityId']) : 0;
if (empty($activityId)) {
\Civi::log()->warning('Activity ID is missing');
return;
}
try {
$activities = \Civi\Api4\Activity::get(FALSE)
->addSelect('source_contact_id')
->addJoin('ActivityContact AS activity_contact', 'LEFT')
->addWhere('id', '=', $activityId)
->addWhere('activity_type_id:label', '=', 'Material Contribution')
->execute();
if ($activities->count() === 0) {
\Civi::log()->info('No activities found for Activity ID:', ['activityId' => $activityId]);
return;
}
$activity = $activities->first();
$individualId = $activity['source_contact_id'];
$contact = \Civi\Api4\Contact::get(FALSE)
->addSelect('contact_sub_type')
->addWhere('id', '=', $individualId)
->execute()
->first();
if (empty($contact)) {
\Civi::log()->info('Contact not found', ['contact' => $contact['id']]);
$activityId = isset( $_GET['activityId'] ) ? intval( $_GET['activityId'] ) : 0;
if ( empty( $activityId ) ) {
\Civi::log()->warning( 'Activity ID is missing' );
return;
}
try {
$activities = \Civi\Api4\Activity::get( false )
->addSelect( 'source_contact_id' )
->addJoin( 'ActivityContact AS activity_contact', 'LEFT' )
->addWhere( 'id', '=', $activityId )
->addWhere( 'activity_type_id:label', '=', 'Material Contribution' )
->execute();
if ( $activities->count() === 0 ) {
\Civi::log()->info( 'No activities found for Activity ID:', array( 'activityId' => $activityId ) );
return;
}
$activity = $activities->first();
$individualId = $activity['source_contact_id'];
$contact = \Civi\Api4\Contact::get( false )
->addSelect( 'contact_sub_type' )
->addWhere( 'id', '=', $individualId )
->execute()
->first();
if ( empty( $contact ) ) {
\Civi::log()->info( 'Contact not found', array( 'contact' => $contact['id'] ) );
return;
}
$contactSubTypes = $contact['contact_sub_type'] ?? array();
// If the individual is already a volunteer, don't show the button
if ( in_array( 'Volunteer', $contactSubTypes ) ) {
return;
}
$redirectPath = '/volunteer-registration/form-with-details/';
$redirectPathWithParams = $redirectPath . '#?' . http_build_query(
array(
'Individual1' => $individualId,
'message' => 'individual-user',
)
);
$buttonText = __( 'Wish to Volunteer?', 'goonj-crm' );
return goonj_generate_button_html( $redirectPathWithParams, $buttonText );
} catch ( \Exception $e ) {
\Civi::log()->error( 'Error in goonj_contribution_volunteer_signup_button: ' . $e->getMessage() );
return;
}
}
function goonj_contribution_volunteer_signup_button() {
$activityId = filter_input(INPUT_GET, 'activityId', FILTER_VALIDATE_INT) ?: 0;
if ( empty( $activityId ) ) {
\Civi::log()->warning( 'Activity ID is missing' );
return;
}
try {
$activities = \Civi\Api4\Activity::get( false )
->addSelect( 'source_contact_id' )
->addJoin( 'ActivityContact AS activity_contact', 'LEFT' )
->addWhere( 'id', '=', $activityId )
->addWhere( 'activity_type_id:label', '=', 'Material Contribution' )
->execute();
if ( $activities->count() === 0 ) {
\Civi::log()->info( 'No activities found for Activity ID:', array( 'activityId' => $activityId ) );
return;
}
$activity = $activities->first();
$individualId = $activity['source_contact_id'];
$contact = \Civi\Api4\Contact::get( false )
->addSelect( 'contact_sub_type' )
->addWhere( 'id', '=', $individualId )
->execute()
->first();
if ( empty( $contact ) ) {
\Civi::log()->info( 'Contact not found', array( 'contact' => $contact['id'] ) );
return;
}
$contactSubTypes = $contact['contact_sub_type'] ?? array();
// If the individual is already a volunteer, don't show the button
if ( in_array( 'Volunteer', $contactSubTypes ) ) {
return;
}
$redirectPath = '/volunteer-registration/form-with-details/';
$redirectPathWithParams = $redirectPath . '#?' . http_build_query(
array(
'Individual1' => $individualId,
'message' => 'individual-user',
)
);
$buttonText = __( 'Wish to Volunteer?', 'goonj-crm' );
return goonj_generate_button_html( $redirectPathWithParams, $buttonText );
} catch ( \Exception $e ) {
\Civi::log()->error( 'Error in goonj_contribution_volunteer_signup_button: ' . $e->getMessage() );
return;
}
}

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Outside diff range and nitpick comments (5)
wp-content/themes/goonj-crm/functions.php (4)

Line range hint 72-101: Consider enhancing security in login validation.

The login validation functions are well-implemented, but there's room for a security improvement:

In the goonj_check_empty_login_fields function, consider using WordPress's wp_safe_redirect() instead of wp_redirect() for added security.

Apply this diff to enhance security:

- wp_redirect( $redirect_url );
+ wp_safe_redirect( $redirect_url );

This change ensures that the redirect URL is safe and prevents potential open redirect vulnerabilities.


Line range hint 103-126: Enhance security in password reset handling.

The password reset functions are well-implemented, but there's room for a security improvement:

In the goonj_custom_password_reset_redirection function, consider using wp_safe_redirect() instead of wp_redirect() for added security.

Apply this diff to enhance security:

- wp_redirect( add_query_arg( 'password-reset', 'success', home_url() ) );
+ wp_safe_redirect( add_query_arg( 'password-reset', 'success', home_url() ) );

This change ensures that the redirect URL is safe and prevents potential open redirect vulnerabilities.


Line range hint 129-399: Refactor user identification form handling for improved maintainability.

The goonj_handle_user_identification_form function is quite complex and handles multiple responsibilities. Consider refactoring this function to improve readability and maintainability:

  1. Extract the logic for different purposes into separate functions.
  2. Use early returns to reduce nesting.
  3. Consider using a strategy pattern or separate handler classes for different purposes.

Here's a high-level example of how you might start refactoring:

function goonj_handle_user_identification_form() {
    if (!should_process_form()) {
        return;
    }

    $user_data = get_user_data_from_post();
    $purpose_handler = get_purpose_handler($user_data['purpose']);
    $purpose_handler->handle($user_data);
}

function should_process_form() {
    return isset($_POST['action']) && $_POST['action'] === 'goonj-check-user';
}

function get_user_data_from_post() {
    // Extract and return user data from $_POST
}

function get_purpose_handler($purpose) {
    $handlers = [
        'material-contribution' => new MaterialContributionHandler(),
        'dropping-center' => new DroppingCenterHandler(),
        // ... other handlers
    ];
    return $handlers[$purpose] ?? new DefaultHandler();
}

// Then create separate classes for each purpose handler

This refactoring would make the code more modular, easier to test, and simpler to maintain.


Line range hint 437-496: Refactor individual creation redirection for improved maintainability.

The goonj_redirect_after_individual_creation function is complex and handles multiple responsibilities. Consider refactoring this function to improve readability and maintainability:

  1. Extract the redirection logic for different creation flows into separate functions.
  2. Use early returns to reduce nesting.
  3. Consider using a strategy pattern or separate handler classes for different creation flows.

Here's a high-level example of how you might start refactoring:

function goonj_redirect_after_individual_creation() {
    if (!should_handle_redirection()) {
        return;
    }

    $individual = get_individual_data();
    $redirect_handler = get_redirect_handler($individual['Individual_fields.Creation_Flow']);
    $redirect_path = $redirect_handler->get_redirect_path($individual);

    if (empty($redirect_path)) {
        return;
    }

    wp_safe_redirect($redirect_path);
    exit;
}

function should_handle_redirection() {
    return isset($_GET['goonjAction']) && $_GET['goonj_action'] === 'individualCreated' && isset($_GET['individualId']);
}

function get_individual_data() {
    // Fetch and return individual data using CiviCRM API
}

function get_redirect_handler($creation_flow) {
    $handlers = [
        'material-contribution' => new MaterialContributionRedirectHandler(),
        'office-visit' => new OfficeVisitRedirectHandler(),
        'office-visit-contribution' => new OfficeVisitContributionRedirectHandler(),
    ];
    return $handlers[$creation_flow] ?? new DefaultRedirectHandler();
}

// Then create separate classes for each redirect handler

This refactoring would make the code more modular, easier to test, and simpler to maintain.

wp-content/themes/goonj-crm/engine/helpers.php (1)

19-19: Enhance readability of the return statement

The return statement in goonj_get_goonj_office_id is complex and may hinder readability. Simplifying it can make the code more maintainable.

Consider refactoring as follows:

if ( array_key_exists( $activityTypeLabel, $officeMapping ) ) {
    $key = $officeMapping[ $activityTypeLabel ];
    return $activity[ $key ] ?? null;
}
return null;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between bb2f361 and e2e98ee.

📒 Files selected for processing (3)
  • wp-content/themes/goonj-crm/engine/helpers.php (1 hunks)
  • wp-content/themes/goonj-crm/engine/shortcodes.php (1 hunks)
  • wp-content/themes/goonj-crm/functions.php (5 hunks)
🧰 Additional context used
🔇 Additional comments (6)
wp-content/themes/goonj-crm/functions.php (6)

Line range hint 3-41: LGTM: File inclusions and script enqueuing look good.

The file inclusions and script enqueuing functions follow WordPress best practices. The code is well-organized and modular.


Line range hint 44-47: LGTM: Theme setup function is appropriate.

The goonj_theme_setup function correctly adds an editor style, following WordPress conventions.


Line range hint 51-70: LGTM: User redirection functions are well-implemented.

The goonj_redirect_logged_in_user_to_civi_dashboard and goonj_custom_login_failed_redirect functions handle user redirections appropriately, following WordPress conventions.


Line range hint 401-423: LGTM: Volunteer induction checking function is well-implemented.

The goonj_is_volunteer_inducted function effectively uses the CiviCRM API to check for completed induction activities. The logic is clear and concise.


Line range hint 425-427: LGTM: Custom message placeholder function is appropriate.

The goonj_custom_message_placeholder function provides a simple and clear way to insert a custom message container in the HTML.


Line range hint 429-435: LGTM: Query variables function is well-implemented.

The goonj_query_vars function correctly adds custom query variables following WordPress conventions.

Comment on lines +14 to +17
$officeMapping = array(
'Material Contribution' => 'Material_Contribution.Goonj_Office',
'Office visit' => 'Office_Visit.Goonj_Processing_Center',
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consolidate activity type mappings to adhere to DRY principles

The arrays $officeMapping and $activityMap both map activity types to related data, leading to duplicate code. Merging these mappings enhances maintainability and reduces redundancy.

Consider unifying the mappings:

$activityMap = array(
    'Office visit' => array(
        'field' => 'Office_Visit.Goonj_Processing_Center',
        'redirectPath' => '/processing-center/office-visit/details/',
        'buttonText' => __( 'Proceed to Office Visit', 'goonj-crm' ),
    ),
    'Material Contribution' => array(
        'field' => 'Material_Contribution.Goonj_Office',
        'redirectPath' => '/processing-center/material-contribution/details/',
        'buttonText' => __( 'Proceed to Material Contribution', 'goonj-crm' ),
    ),
);

Update goonj_get_goonj_office_id:

function goonj_get_goonj_office_id( $activity ) {
    global $activityMap;
    $activityTypeLabel = $activity['activity_type_id:name'];
    $field = $activityMap[ $activityTypeLabel ]['field'] ?? null;
    return $field ? ( $activity[ $field ] ?? null ) : null;
}

And adjust goonj_generate_activity_button accordingly.

Also applies to: 79-90

Comment on lines +25 to +28
$timezone = new \DateTimeZone( 'UTC' );
$today = new \DateTime( 'now', $timezone );
$startOfDay = $today->setTime( 0, 0 )->format( 'Y-m-d H:i:s' );
$endOfDay = $today->setTime( 23, 59, 59 )->format( 'Y-m-d H:i:s' );
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix DateTime mutation causing incorrect date ranges

In goonj_fetch_contact_pu_activities_for_today, the use of $today->setTime() modifies the original $today object. This results in both $startOfDay and $endOfDay having the end time of the day, leading to incorrect activity retrieval.

Apply this diff to correct the date range calculation:

- $startOfDay = $today->setTime( 0, 0 )->format( 'Y-m-d H:i:s' );
- $endOfDay = $today->setTime( 23, 59, 59 )->format( 'Y-m-d H:i:s' );
+ $startOfDay = (clone $today)->setTime( 0, 0 )->format( 'Y-m-d H:i:s' );
+ $endOfDay = (clone $today)->setTime( 23, 59, 59 )->format( 'Y-m-d H:i:s' );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$timezone = new \DateTimeZone( 'UTC' );
$today = new \DateTime( 'now', $timezone );
$startOfDay = $today->setTime( 0, 0 )->format( 'Y-m-d H:i:s' );
$endOfDay = $today->setTime( 23, 59, 59 )->format( 'Y-m-d H:i:s' );
$timezone = new \DateTimeZone( 'UTC' );
$today = new \DateTime( 'now', $timezone );
$startOfDay = (clone $today)->setTime( 0, 0 )->format( 'Y-m-d H:i:s' );
$endOfDay = (clone $today)->setTime( 23, 59, 59 )->format( 'Y-m-d H:i:s' );

Comment on lines +127 to +137
function goonj_collection_camp_landing_page() {
ob_start();
get_template_part( 'templates/collection-landing-page' );
return ob_get_clean();
}

function goonj_collection_camp_past_data() {
ob_start();
get_template_part( 'templates/collection-camp-data' );
return ob_get_clean();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor duplicate code to enhance maintainability

The functions goonj_collection_camp_landing_page and goonj_collection_camp_past_data have similar implementations. Refactor them to reduce code duplication and adhere to the Single Responsibility Principle.

Consider creating a generic function:

function goonj_render_template( $template_name ) {
    ob_start();
    get_template_part( $template_name );
    return ob_get_clean();
}

function goonj_collection_camp_landing_page() {
    return goonj_render_template( 'templates/collection-landing-page' );
}

function goonj_collection_camp_past_data() {
    return goonj_render_template( 'templates/collection-camp-data' );
}

Comment on lines +38 to +44
$activities = \Civi\Api4\Activity::get( false )
->addSelect( 'source_contact_id' )
->addJoin( 'ActivityContact AS activity_contact', 'LEFT' )
->addWhere( 'id', '=', $activity_id )
->addWhere( 'activity_type_id:label', '=', 'Material Contribution' )
->execute();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Encapsulate API calls to improve code readability and reusability

The API calls within goonj_contribution_volunteer_signup_button and goonj_pu_activity_button are directly implemented within the functions. Consider encapsulating these API calls into separate functions or classes to enhance readability and promote code reuse.

This refactoring aligns with best practices by separating concerns and making the codebase easier to maintain.

Also applies to: 95-101

Comment on lines +10 to +13
function goonj_check_user_action( $atts ) {
get_template_part( 'templates/form', 'check-user', array( 'purpose' => $atts['purpose'] ) );
return ob_get_clean();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing ob_start() to initiate output buffering

In the goonj_check_user_action function, ob_start() is not called before get_template_part(), but ob_get_clean() is used afterward. This could lead to unexpected behavior or empty output because the output buffer is not initiated.

Apply this diff to fix the issue:

 function goonj_check_user_action( $atts ) {
+	ob_start();
 	get_template_part( 'templates/form', 'check-user', array( 'purpose' => $atts['purpose'] ) );
 	return ob_get_clean();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function goonj_check_user_action( $atts ) {
get_template_part( 'templates/form', 'check-user', array( 'purpose' => $atts['purpose'] ) );
return ob_get_clean();
}
function goonj_check_user_action( $atts ) {
ob_start();
get_template_part( 'templates/form', 'check-user', array( 'purpose' => $atts['purpose'] ) );
return ob_get_clean();
}

Comment on lines +59 to +62
if ( empty( $contact ) ) {
\Civi::log()->info( 'Contact not found', array( 'contact' => $contact['id'] ) );
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Prevent accessing undefined index when $contact is empty

When $contact is empty, the code attempts to access $contact['id'] in the log message, which can cause a PHP notice due to an undefined index.

Apply this diff to fix the issue:

 if ( empty( $contact ) ) {
-	\Civi::log()->info( 'Contact not found', array( 'contact' => $contact['id'] ) );
+	\Civi::log()->info( 'Contact not found', array( 'contact_id' => $individual_id ) );
 	return;
 }

Committable suggestion was skipped due to low confidence.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 29

🧹 Outside diff range and nitpick comments (14)
wp-content/civi-extensions/goonjcustom/Civi/HelperService.php (2)

5-8: Improve class documentation and consider a more specific class name.

The current class documentation "Collection Camp Outcome Service" doesn't accurately reflect the purpose of the HelperService class. Additionally, the class name HelperService is quite generic.

Consider the following improvements:

  1. Update the class documentation to accurately describe the purpose of this service class.
  2. Consider renaming the class to something more specific, like EmailHelperService or CommunicationHelperService, to better reflect its functionality.

10-16: Enhance method documentation and LGTM for implementation.

The getDefaultFromEmail() method implementation looks good. However, the documentation could be more descriptive.

Consider updating the method documentation to:

/**
 * Get the default "From" email address.
 *
 * @return string Formatted email address in the form "Name" <email@example.com>
 */

This provides more context about what the method does and what it returns.

wp-content/civi-extensions/goonjcustom/Civi/Traits/CollectionSource.php (3)

7-11: Add a PHPDoc comment for the CollectionSource trait.

While the trait name is descriptive, it would be beneficial to add a PHPDoc comment explaining the purpose and functionality of the CollectionSource trait. This will improve code documentation and make it easier for other developers to understand and use the trait.

Here's a suggested PHPDoc comment:

/**
 * Trait CollectionSource
 *
 * This trait provides functionality for managing and retrieving subtype IDs
 * within the CiviCRM framework. It includes methods for caching and checking
 * subtype IDs to optimize performance in subtype-related operations.
 */
trait CollectionSource {
    // ... existing code ...
}

13-27: Enhance documentation and consider constant definitions for getSubtypeId() method.

The getSubtypeId() method implements good practices like caching and clean API query construction. However, there are a few areas for improvement:

  1. Add a PHPDoc comment explaining the method's purpose, return value, and any exceptions it might throw.
  2. The constants ENTITY_NAME and ENTITY_SUBTYPE_NAME are used but not defined in this file. Consider adding them to the trait or documenting where they should be defined.

Here's a suggested improvement:

/**
 * Get the subtype ID for the current entity.
 *
 * This method caches the subtype ID for improved performance in subsequent calls.
 *
 * @return int The subtype ID.
 * @throws \Civi\API\Exception\UnauthorizedException If the API call fails due to permissions.
 */
public static function getSubtypeId() {
    // ... existing code ...
}

Also, consider adding a comment explaining where ENTITY_NAME and ENTITY_SUBTYPE_NAME should be defined:

// Note: ENTITY_NAME and ENTITY_SUBTYPE_NAME should be defined in the class using this trait.

29-39: Improve documentation for isCurrentSubtype() method.

The isCurrentSubtype() method implements good practices like early return and type casting. However, it would benefit from better documentation:

  1. Add a PHPDoc comment explaining the method's purpose, parameters, and return value.
  2. Consider using more explicit type checking for $objectRef['subtype'].

Here's a suggested improvement:

/**
 * Check if the provided object reference matches the current subtype.
 *
 * @param array $objectRef The object reference to check.
 * @return bool TRUE if the object reference matches the current subtype, FALSE otherwise.
 */
private static function isCurrentSubtype($objectRef) {
    if (!isset($objectRef['subtype']) || $objectRef['subtype'] === '') {
        return FALSE;
    }

    $subtypeId = self::getSubtypeId();
    return (int) $objectRef['subtype'] === $subtypeId;
}

This improvement adds clear documentation and uses more explicit type checking for the subtype key in the $objectRef array.

wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/CollectionCampOutcomeReminderCron.php (3)

62-66: Include stack trace in error logs for better debugging

Currently, only the exception message is logged. Including the stack trace can provide more context and assist in diagnosing issues more effectively.

Modify the logging statement to include the stack trace:

 \Civi::log()->error('Error processing camp reminder', [
     'id' => $camp['id'],
     'error' => $e->getMessage(),
+    'trace' => $e->getTraceAsString(),
 ]);

37-37: Remove unused variable $returnValues

The variable $returnValues is initialized but not utilized within the function. Removing it can simplify the code and eliminate unnecessary variables.

You can remove the variable and adjust the return statement:

-$returnValues = [];
-return civicrm_api3_create_success($returnValues, $params, 'Goonjcustom', 'collection_camp_outcome_reminder_cron');
+return civicrm_api3_create_success([], $params, 'Goonjcustom', 'collection_camp_outcome_reminder_cron');

Also applies to: 69-69


57-67: Handle exceptions more granularly

Currently, if an exception occurs while processing a camp reminder, the exception is caught, and the loop continues. Consider implementing additional logic to retry the operation or escalate the issue if certain types of exceptions occur.

Evaluate the types of exceptions that may be thrown and decide if some require immediate attention or different handling strategies. This can improve the robustness of the cron job.

wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/VolunteerFeedbackReminderCron.php (1)

40-70: Add Unit Tests for the Volunteer Feedback Reminder Cron

Unit tests would help ensure that the civicrm_api3_goonjcustom_volunteer_feedback_reminder_cron function behaves as expected under various scenarios, such as when there are camps needing reminders, when exceptions occur, and when there are no camps to process.

Would you like me to assist in creating unit tests for this function or open a new GitHub issue to track this task?

wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2)

9-11: Add descriptive PHPDoc comments for the trait and its methods

The trait QrCodeable and its methods generateQrCode and saveQrCode lack detailed PHPDoc comments. Adding comprehensive documentation, including descriptions of parameters, return values, and any exceptions thrown, will enhance code readability and maintainability.

Also applies to: 14-16, 47-49


70-73: Handle missing custom field more explicitly

When the custom field is not found, the code logs an error and returns FALSE. To improve robustness:

  • Consider throwing an exception to clearly indicate that a critical configuration is missing.
  • Provide a more detailed error message, including the customGroupName and customFieldName, to assist in troubleshooting.

This will make it easier to identify configuration issues during development and maintenance.

wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (1)

61-71: Ensure proper URL concatenation for $data

When constructing the $data URL in generateDroppingCenterQrCode, ensure that $baseUrl ends with a trailing slash to avoid malformed URLs. This attention to detail prevents potential issues with URL formation.

Consider updating the code as follows:

    $baseUrl = \CRM_Core_Config::singleton()->userFrameworkBaseURL;
+   $baseUrl = rtrim($baseUrl, '/') . '/';
    $data = "{$baseUrl}actions/dropping-center/{$id}";

Alternatively, use built-in functions to generate URLs if available.

wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (2)

Line range hint 26-550: Refactor CollectionCampService to adhere to the Single Responsibility Principle

The CollectionCampService class (lines 26-550) currently handles multiple responsibilities, including event subscriptions, QR code generation, email notifications, address handling, and more. This violates the Single Responsibility Principle, making the codebase harder to maintain, test, and extend.

Consider refactoring the class into smaller, focused classes or services, each responsible for a specific domain of functionality. For example:

  • QR Code Generation: Extract QR code-related methods into a separate QrCodeService class.
  • Email Notification: Create an EmailNotificationService to handle all email-related functionality.
  • Event Handling: Use dedicated event subscriber classes for different types of events.

This separation will improve code readability, facilitate unit testing, and enhance maintainability.


20-21: Organize trait imports and usage consistently

The use statements for traits CollectionSource and QrCodeable are added in lines 20-21 and used in lines 27-28. For better code organization and readability, maintain a consistent order and grouping for imports and trait uses.

Consider placing the use statements alphabetically or grouping related traits together to improve clarity.

Also applies to: 27-28

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between e2e98ee and b6a7351.

📒 Files selected for processing (11)
  • wp-content/civi-extensions/goonjcustom/Civi/CollectionCampOutcomeService.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (5 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/CollectionCampVolunteerFeedbackService.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (3 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/HelperService.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/CollectionSource.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/CollectionCampOutcomeReminderCron.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/VolunteerFeedbackReminderCron.php (1 hunks)
  • wp-content/themes/goonj-crm/engine/helpers.php (1 hunks)
  • wp-content/themes/goonj-crm/engine/shortcodes.php (1 hunks)
🧰 Additional context used
📓 Learnings (2)
wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (2)
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-08T09:24:47.239Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-09T05:09:48.296Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.
wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (2)
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-08T09:24:47.239Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-09T05:09:48.296Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.
🔇 Additional comments (9)
wp-content/civi-extensions/goonjcustom/Civi/HelperService.php (2)

1-3: LGTM! Namespace declaration looks good.

The PHP opening tag and namespace declaration follow best practices and PSR-4 autoloading standards.


1-18: LGTM! Consider future expansions for this helper service.

The overall structure of the file is clean and follows the Single Responsibility Principle. As this is a helper service, consider the following suggestions for future improvements:

  1. Add more related helper methods to this class as needed, keeping them focused on email or communication-related functionality.
  2. If the class grows significantly, consider breaking it down into more specific helper classes.
  3. Add unit tests for the getDefaultFromEmail() method and any future methods to ensure reliability.

These suggestions are for future consideration and don't need to be addressed in this PR.

wp-content/civi-extensions/goonjcustom/Civi/Traits/CollectionSource.php (1)

1-5: LGTM: Namespace and use statement are properly defined.

The namespace Civi\Traits and the use statement for Civi\Api4\OptionValue are correctly defined, following PSR-4 autoloading standards. This setup indicates proper integration with CiviCRM's API v4.

wp-content/themes/goonj-crm/engine/helpers.php (1)

6-9: Consider consolidating activity type mappings

As mentioned in a previous review, the arrays $officeMapping and $activityMap both map activity types to related data, leading to duplicate code. Merging these mappings would enhance maintainability and reduce redundancy.

Consider unifying the mappings as suggested in the previous review:

$activityMap = array(
    'Office visit' => array(
        'field' => 'Office_Visit.Goonj_Processing_Center',
        'redirectPath' => '/processing-center/office-visit/details/',
        'buttonText' => __( 'Proceed to Office Visit', 'goonj-crm' ),
        'queryParam' => 'Office_Visit.Goonj_Processing_Center',
    ),
    'Material Contribution' => array(
        'field' => 'Material_Contribution.Goonj_Office',
        'redirectPath' => '/processing-center/material-contribution/details/',
        'buttonText' => __( 'Proceed to Material Contribution', 'goonj-crm' ),
        'queryParam' => 'Material_Contribution.Goonj_Office',
    ),
);

Then update both goonj_get_goonj_office_id and goonj_generate_activity_button to use this unified mapping.

Also applies to: 28-39

wp-content/civi-extensions/goonjcustom/api/v3/Goonjcustom/CollectionCampOutcomeReminderCron.php (1)

54-54: Ensure consistent timezone when comparing dates in queries

When comparing End_Date from the database with $endOfDay, it's crucial to ensure that both are in the same timezone to avoid inaccuracies in the query results due to timezone discrepancies.

Please verify that Collection_Camp_Intent_Details.End_Date is stored in UTC or the same timezone as $endOfDay. If not, you may need to adjust the date comparison or convert timezones accordingly.

wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1)

19-31: ⚠️ Potential issue

Verify the handling of QR code output format

The code removes a base64 header and decodes the image data after generating the QR code. However, when using QRCode::OUTPUT_IMAGE_PNG, the render method should return raw binary PNG data, not a base64-encoded string with a header. This suggests there may be a misconfiguration or misunderstanding of the QRCode library's output options.

Run the following script to inspect the usage of QR code output options in the codebase:

wp-content/civi-extensions/goonjcustom/Civi/CollectionCampOutcomeService.php (1)

93-99: Sanitize variables in email content to prevent XSS vulnerabilities.

Variables like $attendeeName are inserted into the email HTML without sanitization, which can lead to XSS attacks if the data contains malicious input.

[security]

Use htmlspecialchars() to sanitize user input:

-$html = "
-          <p>Dear $attendeeName,</p>
-          <p>This is a kind reminder to complete the Camp Outcome Form at the earliest. Your feedback is crucial in helping us assess the overall response and impact of the camp/drive.</p>
-          <p>You can access the form using the link below:</p>
-          <p><a href=\"$campOutcomeFormUrl\">Camp Outcome Form</a></p>
-          <p>We appreciate your cooperation.</p>
-          <p>Warm Regards,<br>Urban Relations Team</p>";
+$safeAttendeeName = htmlspecialchars($attendeeName, ENT_QUOTES, 'UTF-8');
+$safeCampOutcomeFormUrl = htmlspecialchars($campOutcomeFormUrl, ENT_QUOTES, 'UTF-8');
+$html = "
+          <p>Dear $safeAttendeeName,</p>
+          <p>This is a kind reminder to complete the Camp Outcome Form at the earliest. Your feedback is crucial in helping us assess the overall response and impact of the camp/drive.</p>
+          <p>You can access the form using the link below:</p>
+          <p><a href=\"$safeCampOutcomeFormUrl\">Camp Outcome Form</a></p>
+          <p>We appreciate your cooperation.</p>
+          <p>Warm Regards,<br>Urban Relations Team</p>";
wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (2)

470-472: Ensure isCurrentSubtype method handles exceptions and null values

In the generateCollectionCampQr method (lines 470-472), the condition uses !self::isCurrentSubtype($objectRef). If isCurrentSubtype does not handle null values or exceptions properly, it could cause unexpected behavior or errors.

Verify that isCurrentSubtype is robust against null or malformed $objectRef and includes necessary exception handling:

private static function isCurrentSubtype($objectRef) {
  if (!$objectRef || !isset($objectRef['subtype'])) {
    return FALSE;
  }
  // Additional logic...
}

546-546: 🛠️ Refactor suggestion

Avoid code duplication when regenerating QR codes

In the reGenerateCollectionCampQr method (line 546), the call to self::generateCollectionCampQrCode($collectionCampId); duplicates the logic used elsewhere for QR code generation.

To streamline the code, consider centralizing QR code generation logic in a single method or service. This reduces redundancy and simplifies maintenance.

⛔ Skipped due to learnings
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-09T05:09:48.296Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-08T09:24:47.239Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.

Comment on lines +4 to +14
function goonj_get_goonj_office_id( $activity ) {
$activityTypeName = $activity['activity_type_id:name'];
$officeMapping = array(
'Material Contribution' => 'Material_Contribution.Goonj_Office',
'Office visit' => 'Office_Visit.Goonj_Processing_Center',
);
$officeCustomFieldName = $officeMapping[$activityTypeName];

return $activity[$officeCustomFieldName];
// return array_key_exists( $activityTypeName, $officeMapping ) ? $activity[ $officeMapping[ $activityTypeName ] ] ?? null : null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve error handling and use the safer implementation

The current implementation of goonj_get_goonj_office_id function lacks proper error handling for unknown activity types. This could lead to undefined index errors if an unexpected activity type is provided.

Consider implementing the following improvements:

  1. Use the commented-out line as it provides better error handling:
return array_key_exists( $activityTypeName, $officeMapping ) ? $activity[ $officeMapping[ $activityTypeName ] ] ?? null : null;
  1. Add a logging mechanism to track occurrences of unknown activity types:
if (!array_key_exists($activityTypeName, $officeMapping)) {
    error_log("Unknown activity type encountered: " . $activityTypeName);
    return null;
}

These changes will make the function more robust and easier to debug in case of issues.

$details['queryParam'] => $office_id,
);

$redirectPathWithParams = $details['redirectPath'] . '#?' . http_build_query( $redirectParams );
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix URL construction for proper query parameter handling

The current URL construction in the goonj_generate_activity_button function uses '#?' which is incorrect for query parameters. This may lead to improper redirection.

Please update the URL construction as follows:

$redirectPathWithParams = $details['redirectPath'] . '?' . http_build_query( $redirectParams );

This change ensures that the query parameters are properly appended to the URL.

Comment on lines +17 to +55
function goonj_generate_activity_button( $activity, $office_id, $individual_id ) {
// Define all activity types to check pending activities
$activityTypes = array( 'Office visit', 'Material Contribution' );

// Activity type that contact has already completed
$completedActivityType = $activity['activity_type_id:name'];

// Calculate the pending activities by comparing with $activityTypes
$pendingActivities = array_diff( $activityTypes, array( $completedActivityType ) );

// Define the mapping for each activity to the corresponding button and redirect info
$activityMap = array(
'Office visit' => array(
'redirectPath' => '/processing-center/office-visit/details/',
'buttonText' => __( 'Proceed to Office Visit', 'goonj-crm' ),
'queryParam' => 'Office_Visit.Goonj_Processing_Center',
),
'Material Contribution' => array(
'redirectPath' => '/processing-center/material-contribution/details/',
'buttonText' => __( 'Proceed to Material Contribution', 'goonj-crm' ),
'queryParam' => 'Material_Contribution.Goonj_Office',
),
);

// Get the next pending activity
$nextActivity = reset( $pendingActivities );
$details = $activityMap[ $nextActivity ];

// Prepare the URL with query params
$redirectParams = array(
'source_contact_id' => $individual_id,
$details['queryParam'] => $office_id,
);

$redirectPathWithParams = $details['redirectPath'] . '#?' . http_build_query( $redirectParams );

// Generate and return the button HTML
return goonj_generate_button_html( $redirectPathWithParams, $details['buttonText'] );
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve function robustness and readability

The goonj_generate_activity_button function could benefit from some improvements:

  1. Handle the case where all activities are completed:
if (empty($pendingActivities)) {
    return ''; // or some appropriate message
}
  1. Consider refactoring the activity mapping into a separate constant or configuration file to improve maintainability.

  2. Use early returns to reduce nesting and improve readability:

if (empty($pendingActivities)) {
    return '';
}

$nextActivity = reset($pendingActivities);
if (!isset($activityMap[$nextActivity])) {
    error_log("Unknown activity type encountered: " . $nextActivity);
    return '';
}

$details = $activityMap[$nextActivity];
// ... rest of the function

These changes will make the function more robust and easier to maintain.

Comment on lines +43 to +55
$collectionCamps = EckEntity::get('Collection_Camp', TRUE)
->addSelect(
'Logistics_Coordination.Camp_to_be_attended_by',
'Collection_Camp_Intent_Details.End_Date',
'Camp_Outcome.Last_Reminder_Sent',
'title',
'Collection_Camp_Intent_Details.Location_Area_of_camp',
)
->addWhere('Camp_Outcome.Rate_the_camp', 'IS NULL')
->addWhere('Logistics_Coordination.Email_Sent', '=', 1)
->addWhere('Collection_Camp_Core_Details.Status', '=', 'authorized')
->addWhere('Collection_Camp_Intent_Details.End_Date', '<=', $endOfDay)
->execute();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Abstract query conditions for maintainability

Hardcoding strings like 'authorized' and field names directly in the query can make future modifications error-prone. Consider defining constants or using a configuration file for such values to enhance maintainability and reduce the likelihood of typos.

Define a constant for the status:

const STATUS_AUTHORIZED = 'authorized';

Update the query condition:

->addWhere('Collection_Camp_Core_Details.Status', '=', 'authorized')
->addWhere('Collection_Camp_Core_Details.Status', '=', self::STATUS_AUTHORIZED)

Comment on lines +61 to +66
catch (\Exception $e) {
\Civi::log()->error('Error processing camp reminder', [
'id' => $camp['id'],
'error' => $e->getMessage(),
]);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider catching \Throwable instead of \Exception

Catching \Throwable allows you to handle all types of errors and exceptions, including those that do not extend from \Exception, such as \Error. This ensures that all possible issues are caught and logged.

Update the catch block as follows:

-    catch (\Exception $e) {
+    catch (\Throwable $e) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
catch (\Exception $e) {
\Civi::log()->error('Error processing camp reminder', [
'id' => $camp['id'],
'error' => $e->getMessage(),
]);
}
catch (\Throwable $e) {
\Civi::log()->error('Error processing camp reminder', [
'id' => $camp['id'],
'error' => $e->getMessage(),
]);
}

Comment on lines +137 to +144
$entity = EckEntity::get(self::ENTITY_NAME, TRUE)
->addWhere('id', '=', $entityId)
->execute();

$entity = $entityResults->first();
->execute()->single();

$entitySubtypeValue = $entity['subtype'];
$subtypeId = self::getSubtypeId();

$subtypeResults = OptionValue::get(TRUE)
->addSelect('name')
->addWhere('grouping', '=', self::ENTITY_NAME)
->addWhere('value', '=', $entitySubtypeValue)
->execute();

$subtype = $subtypeResults->first();

if (!$subtype) {
return FALSE;
}

$subtypeName = $subtype['name'];

if ($subtypeName !== self::ENTITY_SUBTYPE_NAME) {
return FALSE;
}

return TRUE;
return (int) $entitySubtypeValue === $subtypeId;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure entity existence before accessing its subtype

In the isViewingDroppingCenter method, if $entity is null, attempting to access $entity['subtype'] will cause an error. Checking whether $entity is not null before accessing its properties enhances code reliability.

Apply this diff to add a null check:

    $entity = EckEntity::get(self::ENTITY_NAME, TRUE)
      ->addWhere('id', '=', $entityId)
      ->execute()->single();
+   if (!$entity) {
+     return FALSE;
+   }

Committable suggestion was skipped due to low confidence.

Comment on lines +33 to +56
public static function generateDroppingCenterQr(string $op, string $objectName, $objectId, &$objectRef) {
if ($objectName !== 'Eck_Collection_Camp' || !$objectId || !self::isCurrentSubtype($objectRef)) {
return;
}

$newStatus = $objectRef['Collection_Camp_Core_Details.Status'] ?? '';
if (!$newStatus) {
return;
}

$collectionCamps = EckEntity::get('Collection_Camp', TRUE)
->addSelect('Collection_Camp_Core_Details.Status', 'Collection_Camp_Core_Details.Contact_Id')
->addWhere('id', '=', $objectId)
->execute();

$currentCollectionCamp = $collectionCamps->first();
$currentStatus = $currentCollectionCamp['Collection_Camp_Core_Details.Status'];
$collectionCampId = $currentCollectionCamp['id'];

// Check for status change.
if ($currentStatus !== $newStatus && $newStatus === 'authorized') {
self::generateDroppingCenterQrCode($collectionCampId);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential null value for $currentCollectionCamp

In the generateDroppingCenterQr method, after fetching $currentCollectionCamp, there is no check to ensure it's not null before accessing its properties. This could lead to a null pointer exception if the entity is not found. It's best practice to verify that $currentCollectionCamp is not null before proceeding.

Apply this diff to add a null check:

    $currentCollectionCamp = $collectionCamps->first();
+   if (!$currentCollectionCamp) {
+     return;
+   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static function generateDroppingCenterQr(string $op, string $objectName, $objectId, &$objectRef) {
if ($objectName !== 'Eck_Collection_Camp' || !$objectId || !self::isCurrentSubtype($objectRef)) {
return;
}
$newStatus = $objectRef['Collection_Camp_Core_Details.Status'] ?? '';
if (!$newStatus) {
return;
}
$collectionCamps = EckEntity::get('Collection_Camp', TRUE)
->addSelect('Collection_Camp_Core_Details.Status', 'Collection_Camp_Core_Details.Contact_Id')
->addWhere('id', '=', $objectId)
->execute();
$currentCollectionCamp = $collectionCamps->first();
$currentStatus = $currentCollectionCamp['Collection_Camp_Core_Details.Status'];
$collectionCampId = $currentCollectionCamp['id'];
// Check for status change.
if ($currentStatus !== $newStatus && $newStatus === 'authorized') {
self::generateDroppingCenterQrCode($collectionCampId);
}
}
public static function generateDroppingCenterQr(string $op, string $objectName, $objectId, &$objectRef) {
if ($objectName !== 'Eck_Collection_Camp' || !$objectId || !self::isCurrentSubtype($objectRef)) {
return;
}
$newStatus = $objectRef['Collection_Camp_Core_Details.Status'] ?? '';
if (!$newStatus) {
return;
}
$collectionCamps = EckEntity::get('Collection_Camp', TRUE)
->addSelect('Collection_Camp_Core_Details.Status', 'Collection_Camp_Core_Details.Contact_Id')
->addWhere('id', '=', $objectId)
->execute();
$currentCollectionCamp = $collectionCamps->first();
if (!$currentCollectionCamp) {
return;
}
$currentStatus = $currentCollectionCamp['Collection_Camp_Core_Details.Status'];
$collectionCampId = $currentCollectionCamp['id'];
// Check for status change.
if ($currentStatus !== $newStatus && $newStatus === 'authorized') {
self::generateDroppingCenterQrCode($collectionCampId);
}
}

Comment on lines +131 to +139
$entity = EckEntity::get(self::ENTITY_NAME, TRUE)
->addWhere('id', '=', $entityId)
->execute();

$entity = $entityResults->first();
->execute()->single();

$entitySubtypeValue = $entity['subtype'];

$subtypeResults = OptionValue::get(TRUE)
->addSelect('name')
->addWhere('grouping', '=', self::ENTITY_NAME)
->addWhere('value', '=', $entitySubtypeValue)
->execute();

$subtype = $subtypeResults->first();

if (!$subtype) {
return FALSE;
}

$subtypeName = $subtype['name'];

if ($subtypeName !== self::ENTITY_SUBTYPE_NAME) {
return FALSE;
}
$subtypeId = self::getSubtypeId();

return TRUE;
return (int) $entitySubtypeValue === $subtypeId;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add null checks to prevent potential null reference errors

In the isViewingCollectionCamp method (lines 131-139), the $entity retrieved may be null if the query returns no results. Accessing $entity['subtype'] without checking can lead to a null reference error.

Ensure that $entity is not null before accessing its properties:

$entity = EckEntity::get(self::ENTITY_NAME, TRUE)
  ->addWhere('id', '=', $entityId)
  ->execute()
  ->single();

if (!$entity) {
  return FALSE;
}

$entitySubtypeValue = $entity['subtype'];

This check prevents potential runtime errors when the entity is not found.

Comment on lines +501 to +510
private static function generateCollectionCampQrCode($id) {
$baseUrl = \CRM_Core_Config::singleton()->userFrameworkBaseURL;
$data = "{$baseUrl}actions/collection-camp/{$id}";

$baseFileName = "qr_code_{$collectionCampId}.png";

$fileName = \CRM_Utils_File::makeFileName($baseFileName);

$tempFilePath = \CRM_Utils_File::tempnam($baseFileName);

$numBytes = file_put_contents($tempFilePath, $qrcode);

if (!$numBytes) {
\CRM_Core_Error::debug_log_message('Failed to write QR code to temporary file for collection camp ID ' . $collectionCampId);
return FALSE;
}

$customFields = CustomField::get(FALSE)
->addSelect('id')
->addWhere('custom_group_id:name', '=', 'Collection_Camp_QR_Code')
->addWhere('name', '=', 'QR_Code')
->setLimit(1)
->execute();

$qrField = $customFields->first();

if (!$qrField) {
\CRM_Core_Error::debug_log_message('No field to save QR Code for collection camp ID ' . $collectionCampId);
return FALSE;
}

$qrFieldId = 'custom_' . $qrField['id'];

// Save the QR code as an attachment linked to the collection camp.
$params = [
'entity_id' => $collectionCampId,
'name' => $fileName,
'mime_type' => 'image/png',
'field_name' => $qrFieldId,
'options' => [
'move-file' => $tempFilePath,
],
];

$result = civicrm_api3('Attachment', 'create', $params);

if (empty($result['id'])) {
\CRM_Core_Error::debug_log_message('Failed to create attachment for collection camp ID ' . $collectionCampId);
return FALSE;
}

$attachment = $result['values'][$result['id']];
$saveOptions = [
'customGroupName' => 'Collection_Camp_QR_Code',
'customFieldName' => 'QR_Code',
];

$attachmentUrl = $attachment['url'];
}
catch (\CiviCRM_API3_Exception $e) {
\CRM_Core_Error::debug_log_message('Error generating QR code: ' . $e->getMessage());
return FALSE;
}
self::generateQrCode($data, $id, $saveOptions);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid unnecessary wrapper methods to reduce code redundancy

The generateCollectionCampQrCode method (lines 501-510) acts as a thin wrapper around generateQrCode, adding minimal additional logic. This may introduce unnecessary complexity and code duplication.

Consider refactoring by directly calling generateQrCode with the appropriate parameters where needed, unless generateCollectionCampQrCode is anticipated to include more logic in the future:

// Instead of calling generateCollectionCampQrCode($id), call generateQrCode($data, $id, $saveOptions) directly.

This adheres to the DRY (Don't Repeat Yourself) principle and simplifies the codebase.

Comment on lines +502 to +503
$baseUrl = \CRM_Core_Config::singleton()->userFrameworkBaseURL;
$data = "{$baseUrl}actions/collection-camp/{$id}";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure $baseUrl ends with a slash to form correct URLs

In the generateCollectionCampQrCode method (lines 502-503), the $data URL may be incorrect if $baseUrl does not end with a slash. This can lead to malformed URLs and unreachable QR code links.

To prevent this issue, ensure that $baseUrl ends with a slash:

$baseUrl = \CRM_Core_Config::singleton()->userFrameworkBaseURL;
+$baseUrl = rtrim($baseUrl, '/') . '/';
$data = "{$baseUrl}actions/collection-camp/{$id}";

This modification guarantees the correct concatenation of the base URL and the endpoint.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$baseUrl = \CRM_Core_Config::singleton()->userFrameworkBaseURL;
$data = "{$baseUrl}actions/collection-camp/{$id}";
$baseUrl = \CRM_Core_Config::singleton()->userFrameworkBaseURL;
$baseUrl = rtrim($baseUrl, '/') . '/';
$data = "{$baseUrl}actions/collection-camp/{$id}";

@nishant22029
Copy link
Copy Markdown
Collaborator Author

Closing this PR due to extra changes moved changes in this pr - #368

@nishant22029 nishant22029 deleted the feat/add-redirection-button-for-pu-flow branch October 15, 2024 08:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants