Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ZMS-3253 ZMS-3466 ZMS-3415 ZMS-1891): Replicate frontend validation in the backend for the availability opening hours to improve data integrity and frontend validation messaging #803

Open
wants to merge 112 commits into
base: next
Choose a base branch
from

Conversation

ThomasAFink
Copy link
Member

@ThomasAFink ThomasAFink commented Jan 28, 2025

Pull Request Checklist (Feature Branch to next):

  • Ich habe die neuesten Änderungen aus dem next Branch in meinen Feature-Branch gemergt.
  • Das Code-Review wurde abgeschlossen.
  • Fachliche Tests wurden durchgeführt und sind abgeschlossen.

Summary by CodeRabbit

Based on the comprehensive summary of changes across multiple files, here are the high-level release notes:

  • New Features

    • Enhanced availability conflict detection and handling.
    • Improved validation for availability entries.
    • More robust error messaging for availability-related operations.
    • New state management for visibility in the SaveBar component.
    • Added new properties and methods for better data handling and validation.
  • Bug Fixes

    • Fixed issues with overlapping availability times.
    • Improved handling of invalid input data for availability entries.
    • Corrected slot time calculations.
  • Improvements

    • Added more detailed validation checks for start and end times.
    • Enhanced error handling with specific exception classes.
    • Refined user interface for displaying conflicts and errors.
    • Updated test coverage for availability management.
    • Streamlined rendering logic for conflict messages.
  • Performance

    • Optimized conflict detection logic.
    • Streamlined availability data processing.

These changes primarily focus on improving the reliability and user experience of availability management in the system.

Tom Fink added 30 commits November 12, 2024 12:25
@it-at-m it-at-m deleted a comment from coderabbitai bot Jan 29, 2025
@ThomasAFink
Copy link
Member Author

ThomasAFink commented Jan 29, 2025

Seems like the problem with adjacent marked as overlapped also exists in checkForOverlaps in AvailabilityConflicts @coderabbitai

<?php
/**
 * @package Zmsadmin
 * @copyright BerlinOnline Stadtportal GmbH & Co. KG
 **/

namespace BO\Zmsadmin;

use BO\Zmsapi\Exception\BadRequest as BadRequestException;
use BO\Zmsentities\Collection\AvailabilityList;
use BO\Zmsentities\Process;

/**
 * Check if new Availability is in conflict with existing availability
 */
class AvailabilityConflicts extends BaseController
{
    /**
     * @SuppressWarnings(Param)
     * @return String
     */
    public function readResponse(
        \Psr\Http\Message\RequestInterface $request,
        \Psr\Http\Message\ResponseInterface $response,
        array $args
    ) {
        $validator = $request->getAttribute('validator');
        $input = $validator->getInput()->isJson()->assertValid()->getValue();
        $data = static::getAvailabilityData($input);
        return \BO\Slim\Render::withJson($response, $data);
    }

    protected static function getAvailabilityData($input)
    {
        if (!isset($input['availabilityList']) || !is_array($input['availabilityList'])) {
            throw new BadRequestException('Missing or invalid availabilityList.');
        } else if(!isset($input['availabilityList'][0]['scope'])){
            throw new BadRequestException('Missing or invalid scope.');
        } else if (!isset($input['selectedDate'])) {
            throw new BadRequestException("'selectedDate' is required.");
        }

        $conflictList = new \BO\Zmsentities\Collection\ProcessList();
        $availabilityList = (new AvailabilityList())->addData($input['availabilityList']);
        $conflictedList = [];
    
        $selectedDateTime = (new \DateTimeImmutable($input['selectedDate']))->modify(\App::$now->format('H:i:s'));

        // First check overlaps between new availabilities
        $overlapConflicts = self::checkForOverlaps($availabilityList, $selectedDateTime);
        $conflictList->addList($overlapConflicts);
    
        $scopeData = $input['availabilityList'][0]['scope'];
        $scope = new \BO\Zmsentities\Scope($scopeData);

        $futureAvailabilityList = self::getAvailabilityList($scope, $selectedDateTime);
    
        foreach ($futureAvailabilityList as $futureAvailability) {
            $availabilityList->addEntity($futureAvailability);
        }
    
        $originId = null;
        foreach ($availabilityList as $availability) {
            if (isset($availability->kind) && $availability->kind === 'origin' && isset($availability->id)) {
                $originId = $availability->id;
                break;
            }
        }
    
        $filteredAvailabilityList = new AvailabilityList();
        foreach ($availabilityList as $availability) {
            if ((!isset($availability->kind) || $availability->kind !== 'exclusion') && 
                (!isset($availability->id) || $availability->id !== $originId)) {
                $filteredAvailabilityList->addEntity($availability);
            }
        }
    
        [$earliestStartDateTime, $latestEndDateTime] = $filteredAvailabilityList->getDateTimeRangeFromList($selectedDateTime);
    
        $filteredAvailabilityList = $filteredAvailabilityList->sortByCustomStringKey('endTime');
        $existingConflicts = $filteredAvailabilityList->getConflicts($earliestStartDateTime, $latestEndDateTime);
        $conflictList->addList($existingConflicts);
    
        foreach ($conflictList as $conflict) {
            $availabilityId = ($conflict->getFirstAppointment()->getAvailability()->getId()) ?
                $conflict->getFirstAppointment()->getAvailability()->getId() :
                $conflict->getFirstAppointment()->getAvailability()->tempId;
            if (!in_array($availabilityId, $conflictedList)) {
                $conflictedList[] = $availabilityId;
            }
        }
    
        return [
            'conflictList' => $conflictList->toConflictListByDay(),
            'conflictIdList' => (count($conflictedList)) ? $conflictedList : []
        ];
    }

    /**
     * Check for overlaps between availabilities in the collection
     * 
     * @param AvailabilityList $collection
     * @param \DateTimeImmutable $selectedDateTime
     * @return \BO\Zmsentities\Collection\ProcessList
     */
    protected static function checkForOverlaps(AvailabilityList $collection, \DateTimeImmutable $selectedDateTime)
    {
        $conflicts = new \BO\Zmsentities\Collection\ProcessList();
        
        foreach ($collection as $availability1) {
            foreach ($collection as $availability2) {
                $scope1Id = is_array($availability1->scope) ? ($availability1->scope['id'] ?? null) : ($availability1->scope->id ?? null);
                $scope2Id = is_array($availability2->scope) ? ($availability2->scope['id'] ?? null) : ($availability2->scope->id ?? null);
                
                if ($availability1 !== $availability2 && 
                    $availability1->type == $availability2->type &&
                    $scope1Id == $scope2Id &&
                    $availability1->hasSharedWeekdayWith($availability2)) {
                    
                    $start1 = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)
                        ->modify($availability1->startTime);
                    $end1 = (new \DateTimeImmutable())->setTimestamp($availability1->endDate)
                        ->modify($availability1->endTime);
                    $start2 = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)
                        ->modify($availability2->startTime);
                    $end2 = (new \DateTimeImmutable())->setTimestamp($availability2->endDate)
                        ->modify($availability2->endTime);

                    if ($start1 < $end2 && $start2 < $end1) {
                        $process = new Process();
                        $process->amendment = 'Neue Öffnungszeiten überschneiden sich.';
                        $appointment = new \BO\Zmsentities\Appointment();
                        $appointment->date = $availability1->startDate;
                        $appointment->availability = $availability1;
                        $process->addAppointment($appointment);
                        $conflicts->addEntity($process);
                    }
                }
            }
        }
        
        return $conflicts;
    }

    /**
     * Fetch availabilities for a given scope and date.
     * 
     * @param \BO\Zmsentities\Scope $scope
     * @param \DateTimeImmutable $dateTime
     * @return AvailabilityList
     */
    protected static function getAvailabilityList($scope, $dateTime)
    {
        try {
            $availabilityList = \App::$http
                ->readGetResult(
                    '/scope/' . $scope->getId() . '/availability/',
                    [
                        'resolveReferences' => 0,
                        'startDate' => $dateTime->format('Y-m-d') // Only fetch availabilities from this date onward
                    ]
                )
                ->getCollection();
        } catch (\BO\Zmsclient\Exception $exception) {
            if ($exception->template != 'BO\Zmsapi\Exception\Availability\AvailabilityNotFound') {
                throw $exception;
            }
            $availabilityList = new \BO\Zmsentities\Collection\AvailabilityList();
        }
        return $availabilityList->withScope($scope);
    }
}

Copy link
Contributor

coderabbitai bot commented Jan 29, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fixed version of the checkForOverlaps method:

protected static function checkForOverlaps(AvailabilityList $collection, \DateTimeImmutable $selectedDateTime)
{
    $conflicts = new \BO\Zmsentities\Collection\ProcessList();
    
    foreach ($collection as $availability1) {
        foreach ($collection as $availability2) {
            $scope1Id = is_array($availability1->scope) ? ($availability1->scope['id'] ?? null) : ($availability1->scope->id ?? null);
            $scope2Id = is_array($availability2->scope) ? ($availability2->scope['id'] ?? null) : ($availability2->scope->id ?? null);
            
            if ($availability1 !== $availability2 && 
                $availability1->type == $availability2->type &&
                $scope1Id == $scope2Id &&
                $availability1->hasSharedWeekdayWith($availability2)) {
                
                // First check if dates are the same
                $date1 = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)->format('Y-m-d');
                $date2 = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)->format('Y-m-d');
                
                if ($date1 === $date2) {
                    // Compare times as strings for exact boundary handling
                    $time1Start = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)
                        ->modify($availability1->startTime)->format('H:i');
                    $time1End = (new \DateTimeImmutable())->setTimestamp($availability1->endDate)
                        ->modify($availability1->endTime)->format('H:i');
                    $time2Start = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)
                        ->modify($availability2->startTime)->format('H:i');
                    $time2End = (new \DateTimeImmutable())->setTimestamp($availability2->endDate)
                        ->modify($availability2->endTime)->format('H:i');

                    if ($time2Start < $time1End && $time1Start < $time2End) {
                        $process = new Process();
                        $process->amendment = 'Neue Öffnungszeiten überschneiden sich.';
                        $appointment = new \BO\Zmsentities\Appointment();
                        $appointment->date = $availability1->startDate;
                        $appointment->availability = $availability1;
                        $process->addAppointment($appointment);
                        $conflicts->addEntity($process);
                    }
                }
            }
        }
    }
    
    return $conflicts;
}

Key changes:

  1. Added date comparison before checking times
  2. Compare times as strings in H:i format for exact boundary handling
  3. This will now correctly handle adjacent times like 14:00-15:00 and 15:00-17:00

✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/803

Timestamp: 2025-01-29T13:15:50.859Z
Learning: In the eappointment system's availability validation, when checking for overlaps in AvailabilityConflicts, first compare dates in Y-m-d format, then compare times as strings in H:i format to properly handle adjacent times and avoid false positives.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@ThomasAFink
Copy link
Member Author

@coderabbitai how can we rename checkForOverlaps so we know it's only comparing new ones and not existing ones give ten examples for names and maybe rename getConflicts for we know it's the check for existing.

Copy link
Contributor

coderabbitai bot commented Jan 29, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here are 10 suggested names for each method:

For checkForOverlaps (checking between new availabilities):

  1. checkNewAvailabilityOverlaps
  2. validatePendingAvailabilities
  3. checkOverlapsBetweenNewAvailabilities
  4. validateNewAvailabilityPairs
  5. checkIncomingAvailabilityOverlaps
  6. validatePendingAvailabilityOverlaps
  7. checkNewVsNewOverlaps
  8. validateIncomingAvailabilityPairs
  9. checkOverlapsWithinNewSet
  10. validateNewAvailabilityBatch

For getConflicts (checking against existing availabilities):

  1. checkExistingAvailabilityConflicts
  2. validateAgainstExistingAvailabilities
  3. checkOverlapsWithExisting
  4. validateAgainstStoredAvailabilities
  5. checkExistingTimeSlotConflicts
  6. validateAgainstCurrentAvailabilities
  7. checkNewVsExistingOverlaps
  8. validateAgainstActiveAvailabilities
  9. checkOverlapsWithDatabase
  10. validateAgainstSavedAvailabilities

My recommendation would be:

  • Rename checkForOverlaps to checkNewVsNewOverlaps
  • Rename getConflicts to checkNewVsExistingOverlaps

This makes it immediately clear which availabilities are being compared in each method.

🐰 thoughtfully taking notes


✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/803

Timestamp: 2025-01-29T13:25:23.456Z
Learning: In the eappointment system's availability validation, method names should clearly indicate whether they're checking overlaps between new availabilities (e.g., checkNewVsNewOverlaps) or against existing availabilities (e.g., checkNewVsExistingOverlaps) to improve code readability and maintainability.

@ThomasAFink
Copy link
Member Author

  • Rename getConflicts to checkNewVsExistingOverlaps

@coderabbitai but getConflicts doesn't just check new against existing it also checks existing against existing.

Copy link
Contributor

coderabbitai bot commented Jan 29, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink You're absolutely right, I apologize for the confusion. Since getConflicts checks all availabilities against each other (both new vs existing and existing vs existing), a better name would be checkAllAvailabilityOverlaps. This makes it clear that it performs a comprehensive check of all availabilities in the collection, regardless of whether they're new or existing.

So the final naming would be:

  • checkNewVsNewOverlaps (for the new-only check)
  • checkAllAvailabilityOverlaps (for the comprehensive check)

✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/803

Timestamp: 2025-01-29T13:54:38.026Z
Learning: In the eappointment system's availability validation, method names should accurately reflect their scope - use checkNewVsNewOverlaps for checking only between new availabilities, and checkAllAvailabilityOverlaps for checking all availabilities (both new and existing) against each other.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@ThomasAFink
Copy link
Member Author

@coderabbitai we seem to have the same problem with adjacent opening hours in the check against existing opening hours

Folgende Zeiten führen mit der aktuellen Auswahl zu Konflikten: Konflikt: Eine neue Öffnungszeit überschneidet sich mit einer bestehenden Öffnungszeit. Bestehende Öffnungszeit:  [29.01.2025 - 29.01.2025, 14:00 - 15:00] Neue Öffnungszeit:                 [29.01.2025 - 29.01.2025, 15:00 - 17:00]

can you check the methods with check against existing

@coderabbitai now it now longer disables the exact slot 15:00 in the frontend time picker. if two existing availabilities exist

14:00-15:00 and 15:00-16:00

when I go to add a new one I can select 15:00 slot 14:10-14:50 and 15:10-15:00 and are correctly disabled. But should the timestamp sandwiched between two adjacent opening hours be disabled?

Copy link
Contributor

coderabbitai bot commented Jan 29, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Yes, the 15:00 timestamp should be disabled in the time picker when it's sandwiched between two adjacent opening hours. Even though it's technically a boundary point, it's already "allocated" by both the ending time of one availability and the starting time of another. Allowing it to be selected could lead to confusion and potential data integrity issues.

Let me know if you'd like me to help implement this change in the time picker's validation logic.


✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/803

Timestamp: 2025-01-29T14:03:38.192Z
Learning: In the eappointment system's time picker, when a timestamp is exactly at the boundary between two adjacent availabilities (e.g., 15:00 between 14:00-15:00 and 15:00-16:00), that timestamp should be disabled to prevent ambiguity and maintain data integrity.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@ThomasAFink
Copy link
Member Author

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Yes, the 15:00 timestamp should be disabled in the time picker when it's sandwiched between two adjacent opening hours. Even though it's technically a boundary point, it's already "allocated" by both the ending time of one availability and the starting time of another. Allowing it to be selected could lead to confusion and potential data integrity issues.

Let me know if you'd like me to help implement this change in the time picker's validation logic.

✏️ Learnings added

@coderabbitai give full code for this special case

Copy link
Contributor

coderabbitai bot commented Jan 29, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the updated setExcludeTimesForDay method to handle the boundary timestamps:

setExcludeTimesForDay() {
    if (this.state.kind === 'exclusion') {
        return;
    }
    var times = [];
    const availabilities = [...this.state.availabilityList]
        .filter(availability => 
            availability.id !== this.state.availability.id &&
            availability.type == this.state.availability.type &&
            this.isWeekDaySelected(this.state.selectedDate, availability)
        )
        .sort((a, b) => {
            const timeA = moment(a.startTime, 'HH:mm');
            const timeB = moment(b.startTime, 'HH:mm');
            return timeA.diff(timeB);
        });

    // First add regular excluded times
    availabilities.forEach(availability => {
        const startTime = moment(availability.startTime, 'hh:mm')
            .add(this.state.availability.slotTimeInMinutes, "m");
        const startOnDay = moment(this.state.selectedDate)
            .set({"h": startTime.hours(), "m": startTime.minutes()})
            .toDate();
        
        const endTime = moment(availability.endTime, 'hh:mm')
            .subtract(this.state.availability.slotTimeInMinutes, "m");
        const endOnDay = moment(this.state.selectedDate)
            .set({"h": endTime.hours(), "m": endTime.minutes()})
            .toDate();
        
        var currentTime = new Date(startOnDay);
        while (currentTime < endOnDay) {
            times.push(new Date(currentTime));
            currentTime = moment(currentTime)
                .add(this.state.availability.slotTimeInMinutes, "m")
                .toDate();
        }
        times.push(endOnDay);
    });

    // Then check for and add boundary timestamps between adjacent availabilities
    for (let i = 0; i < availabilities.length - 1; i++) {
        const current = availabilities[i];
        const next = availabilities[i + 1];
        
        const currentEnd = moment(current.endTime, 'HH:mm');
        const nextStart = moment(next.startTime, 'HH:mm');
        
        // If they're adjacent (end time of one equals start time of next)
        if (currentEnd.format('HH:mm') === nextStart.format('HH:mm')) {
            // Add the boundary timestamp to excluded times
            const boundaryTime = moment(this.state.selectedDate)
                .set({"h": currentEnd.hours(), "m": currentEnd.minutes()})
                .toDate();
            times.push(boundaryTime);
        }
    }

    this.setState({excludeTimeList: times});
}

Key changes:

  1. Sort availabilities by start time to easily find adjacent ones
  2. Add a second pass to check for adjacent availabilities
  3. When found, add their boundary timestamp to the excluded times
  4. This will disable timestamps like 15:00 when there are availabilities 14:00-15:00 and 15:00-16:00

✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/803

Timestamp: 2025-01-29T14:10:23.456Z
Learning: In the eappointment system's time picker, when building the excludeTimeList, sort availabilities by start time and add boundary timestamps between adjacent availabilities to the excluded times to prevent selection of timestamps that are already allocated.

🧠 Learnings used
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-01-29T14:03:38.620Z
Learning: In the eappointment system's time picker, when a timestamp is exactly at the boundary between two adjacent availabilities (e.g., 15:00 between 14:00-15:00 and 15:00-16:00), that timestamp should be disabled to prevent ambiguity and maintain data integrity.
🔗 Analysis chain
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Find time picker related files
echo "=== Files containing time picker implementation ==="
rg -l "timepicker|TimePicker|filterPassedTime|filterTime" --type js --glob "zmsadmin/js/**"

echo -e "\n=== Content of datepicker.js ==="
cat zmsadmin/js/page/availabilityDay/form/datepicker.js

echo -e "\n=== Looking for time validation logic ==="
rg "filterPassedTime|filterTime|excludeTimes" --type js --glob "zmsadmin/js/**"

Length of output: 16480


🧠 Learnings used
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-01-29T14:03:38.620Z
Learning: In the eappointment system's time picker, when a timestamp is exactly at the boundary between two adjacent availabilities (e.g., 15:00 between 14:00-15:00 and 15:00-16:00), that timestamp should be disabled to prevent ambiguity and maintain data integrity.

Copy link
Contributor

@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

♻️ Duplicate comments (5)
zmsapi/src/Zmsapi/AvailabilityAdd.php (1)

141-159: 🛠️ Refactor suggestion

Add transaction handling for entity updates.

The entity update logic should be wrapped in a transaction to ensure data consistency.

zmsapi/src/Zmsapi/AvailabilityUpdate.php (3)

104-105: 🛠️ Refactor suggestion

Remove commented code and implement proper logging.

Replace commented error logging with proper logging using a logging framework.

Also applies to: 129-130


133-138: 🛠️ Refactor suggestion

Consider wrapping updates in a transaction.

Multiple entity updates should be wrapped in a transaction to ensure data consistency.


184-211: 🛠️ Refactor suggestion

Duplicate conflict checking logic.

The checkNewVsNewConflicts method is duplicated across multiple classes.

zmsentities/src/Zmsentities/Collection/AvailabilityList.php (1)

195-234: 🛠️ Refactor suggestion

Add null checks and improve type safety.

The implementation needs defensive programming to handle edge cases and improve type safety.

The previous review already suggested adding null checks. Additionally:

  1. Consider using a more specific return type hint: array{0: ?\DateTimeImmutable, 1: ?\DateTimeImmutable}
  2. Handle potential timezone issues by explicitly setting the timezone when creating DateTime objects.
- public function getDateTimeRangeFromList(\DateTimeImmutable $selectedDate): array
+ public function getDateTimeRangeFromList(\DateTimeImmutable $selectedDate): array{0: ?\DateTimeImmutable, 1: ?\DateTimeImmutable}
 {
     $earliestStartDateTime = null;
     $latestEndDateTime = null;
+    
+    if ($this->count() === 0) {
+        return [null, null];
+    }
+
+    $timezone = new \DateTimeZone(date_default_timezone_get());

     foreach ($this as $availability) {
+        if (!isset($availability->startDate) || !isset($availability->endDate) ||
+            !isset($availability->startTime) || !isset($availability->endTime)) {
+            continue;
+        }
+
         // Convert Unix timestamp to date strings
-        $startDate = (new \DateTimeImmutable())->setTimestamp($availability->startDate)->format('Y-m-d');
-        $endDate = (new \DateTimeImmutable())->setTimestamp($availability->endDate)->format('Y-m-d');
+        $startDate = (new \DateTimeImmutable('now', $timezone))->setTimestamp($availability->startDate)->format('Y-m-d');
+        $endDate = (new \DateTimeImmutable('now', $timezone))->setTimestamp($availability->endDate)->format('Y-m-d');

         // Combine date and time for start and end
-        $startDateTime = new \DateTimeImmutable("{$startDate} {$availability->startTime}");
-        $endDateTime = new \DateTimeImmutable("{$endDate} {$availability->endTime}");
+        $startDateTime = \DateTimeImmutable::createFromFormat('Y-m-d H:i', "{$startDate} {$availability->startTime}", $timezone);
+        $endDateTime = \DateTimeImmutable::createFromFormat('Y-m-d H:i', "{$endDate} {$availability->endTime}", $timezone);
+
+        if ($startDateTime === false || $endDateTime === false) {
+            continue;
+        }
🧹 Nitpick comments (9)
zmsadmin/js/page/availabilityDay/form/datepicker.js (3)

127-161: Optimize performance and add validation for slot time.

The time exclusion logic can be improved for better performance and reliability:

  1. Use a Set to store unique times instead of an array.
  2. Add validation for slotTimeInMinutes.

Consider applying these improvements:

 var times = [];
+const timeSet = new Set();
+
+if (!this.state.availability.slotTimeInMinutes) {
+    console.warn('slotTimeInMinutes is not defined');
+    return;
+}

 // ... rest of the filtering and sorting logic ...

 while (currentTime < endOnDay) {
-    times.push(new Date(currentTime));
+    timeSet.add(new Date(currentTime).getTime());
     currentTime = moment(currentTime)
         .add(this.state.availability.slotTimeInMinutes, "m")
         .toDate();
 }
-times.push(endOnDay);
+timeSet.add(endOnDay.getTime());

+// Convert Set back to array of Date objects
+times = Array.from(timeSet).map(timestamp => new Date(timestamp));

164-180: Improve boundary timestamp handling.

The boundary timestamp logic is correct but could be more efficient by utilizing the existing timeSet.

Consider this optimization:

 if (currentEnd.format('HH:mm') === nextStart.format('HH:mm')) {
     const boundaryTime = moment(this.state.selectedDate)
         .set({"h": currentEnd.hours(), "m": currentEnd.minutes()})
         .toDate();
-    times.push(boundaryTime);
+    timeSet.add(boundaryTime.getTime());
 }

287-296: Enhance time comparison logic.

The current implementation could miss edge cases and can be more concise using moment's built-in comparison methods.

Consider this improvement:

 if (!moment(this.state.selectedDate).isSame(moment.unix(this.props.attributes.today), 'day')) {
     return true;
 }

 const currentTime = moment();
 const timeToCheck = moment(time);
 
-return timeToCheck.hour() > currentTime.hour() || 
-       (timeToCheck.hour() === currentTime.hour() && timeToCheck.minute() > currentTime.minute());
+return timeToCheck.isSameOrAfter(currentTime, 'minute');
zmsdb/src/Zmsdb/Process.php (1)

495-495: Method name change improves clarity.

The renamed method checkAllVsExistingConflicts better describes its functionality compared to the previous getConflicts.

Consider adding PHPDoc comments to document the method's behavior and return type.

zmsadmin/src/Zmsadmin/AvailabilityConflicts.php (3)

35-41: Robust input validation improves error handling.

The added validation checks ensure all required data is present and valid before processing.

Consider extracting these validation checks into a separate method for better reusability:

+    protected function validateInput($input): void
+    {
+        if (!isset($input['availabilityList']) || !is_array($input['availabilityList'])) {
+            throw new BadRequestException('Missing or invalid availabilityList.');
+        } else if(!isset($input['availabilityList'][0]['scope'])){
+            throw new BadRequestException('Missing or invalid scope.');
+        } else if (!isset($input['selectedDate'])) {
+            throw new BadRequestException("'selectedDate' is required.");
+        }
+    }

49-82: Improved conflict detection with better filtering.

The code now properly handles filtering of availabilities based on kind and ID, making conflict detection more accurate.

Consider extracting the filtering logic into a separate method for better maintainability:

+    protected function filterAvailabilities(AvailabilityList $list, ?string $originId = null): AvailabilityList
+    {
+        $filtered = new AvailabilityList();
+        foreach ($list as $availability) {
+            if ((!isset($availability->kind) || $availability->kind !== 'exclusion') && 
+                (!isset($availability->id) || $availability->id !== $originId)) {
+                $filtered->addEntity($availability);
+            }
+        }
+        return $filtered;
+    }

106-149: Well-structured overlap detection logic.

The new checkNewVsNewConflicts method thoroughly checks for overlaps between availabilities, including proper scope comparison and time boundary handling.

Consider adding early returns to reduce nesting:

     protected static function checkNewVsNewConflicts(AvailabilityList $collection, \DateTimeImmutable $selectedDateTime)
     {
         $conflicts = new \BO\Zmsentities\Collection\ProcessList();
         
         foreach ($collection as $availability1) {
             foreach ($collection as $availability2) {
                 if ($availability1 === $availability2) continue;
                 
                 $scope1Id = is_array($availability1->scope) ? ($availability1->scope['id'] ?? null) : ($availability1->scope->id ?? null);
                 $scope2Id = is_array($availability2->scope) ? ($availability2->scope['id'] ?? null) : ($availability2->scope->id ?? null);
                 
-                if ($availability1 !== $availability2 && 
-                    $availability1->type == $availability2->type &&
-                    $scope1Id == $scope2Id &&
-                    $availability1->hasSharedWeekdayWith($availability2)) {
+                if ($availability1->type != $availability2->type ||
+                    $scope1Id != $scope2Id ||
+                    !$availability1->hasSharedWeekdayWith($availability2)) {
+                    continue;
+                }
zmsentities/src/Zmsentities/Collection/AvailabilityList.php (2)

164-193: Enhance input validation and performance.

The validation implementation is well-structured but could benefit from some improvements:

 public function validateInputs(\DateTimeImmutable $startDate, \DateTimeImmutable $endDate, \DateTimeImmutable $selectedDate, string $kind, $startInDays, $endInDays): array
 {
+    // Early return for empty collection
+    if ($this->count() === 0) {
+        return [];
+    }
+
+    // Validate kind parameter
+    $allowedKinds = ['appointment', 'openinghours']; // Add all valid kinds
+    if (!in_array($kind, $allowedKinds)) {
+        return ['Invalid validation kind specified'];
+    }
+
     $errorList = [];
     $today = new \DateTimeImmutable();
     $yesterday = $selectedDate->modify('-1 day');
     $tomorrow = $selectedDate->modify('+1 day');

+    // Pre-calculate validation parameters to avoid repeated calculations
+    $validationContext = [
+        'today' => $today,
+        'yesterday' => $yesterday,
+        'tomorrow' => $tomorrow,
+        'startInDays' => (int)$startInDays,
+        'endInDays' => (int)$endInDays
+    ];

     foreach ($this as $availability) {
         $errorList = array_merge($errorList,
             $availability->validateStartTime($today, $tomorrow, $startDate, $endDate, $selectedDate, $kind),
             $availability->validateEndTime($startDate, $endDate),
             $availability->validateOriginEndTime($today, $yesterday, $startDate, $endDate, $selectedDate, $kind),
             $availability->validateType($kind),
             $availability->validateSlotTime($startDate, $endDate),
-            $availability->validateBookableDayRange((int)$startInDays, (int)$endInDays)
+            $availability->validateBookableDayRange($validationContext['startInDays'], $validationContext['endInDays'])
         );
     }
     return $errorList;
 }

Line range hint 236-266: Optimize conflict detection and improve documentation.

The conflict detection implementation could be optimized and better documented.

+    /**
+     * Checks for conflicts between all availabilities in the list for a given date range
+     *
+     * @param \DateTimeInterface $startDate The start date of the range to check
+     * @param \DateTimeInterface $endDate The end date of the range to check
+     * @return ProcessList List of conflicts found
+     */
     public function checkAllVsExistingConflicts($startDate, $endDate)
     {
         $processList = new ProcessList();
+        // Pre-filter availabilities that overlap with the date range
+        $relevantAvailabilities = array_filter($this->getArrayCopy(), function($availability) use ($startDate, $endDate) {
+            return $availability->startDate <= $endDate->getTimestamp() && 
+                   $availability->endDate >= $startDate->getTimestamp();
+        });
+
-        foreach ($this as $availability) {
+        foreach ($relevantAvailabilities as $availability) {
             $conflict = $availability->getConflict();
             $currentDate = $startDate;
             while ($currentDate <= $endDate) {

Consider using a more efficient data structure or algorithm for large datasets, such as an interval tree, to reduce the time complexity from O(n²) to O(n log n).

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4043a66 and 860abc6.

📒 Files selected for processing (9)
  • zmsadmin/js/page/availabilityDay/form/datepicker.js (3 hunks)
  • zmsadmin/src/Zmsadmin/AvailabilityConflicts.php (4 hunks)
  • zmsapi/src/Zmsapi/AvailabilityAdd.php (3 hunks)
  • zmsapi/src/Zmsapi/AvailabilityUpdate.php (4 hunks)
  • zmsdb/src/Zmsdb/Process.php (1 hunks)
  • zmsdb/tests/Zmsdb/ProcessConflictTest.php (3 hunks)
  • zmsentities/src/Zmsentities/Availability.php (5 hunks)
  • zmsentities/src/Zmsentities/Collection/AvailabilityList.php (2 hunks)
  • zmsentities/tests/Zmsentities/AvailabilityTest.php (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • zmsdb/tests/Zmsdb/ProcessConflictTest.php
  • zmsentities/src/Zmsentities/Availability.php
🔇 Additional comments (9)
zmsadmin/js/page/availabilityDay/form/datepicker.js (1)

Line range hint 23-23: Update maxTime to 22:00.

The maxTime in the constructor should be set to 22:00 as previously discussed.

zmsadmin/src/Zmsadmin/AvailabilityConflicts.php (2)

9-11: LGTM! Improved error handling with specific exceptions.

The addition of BadRequestException and explicit imports improves error handling clarity.


151-157: Well-documented method with clear return type.

The PHPDoc for getAvailabilityList clearly describes parameters and return type.

zmsentities/src/Zmsentities/Collection/AvailabilityList.php (1)

21-21: Verify data migration for workstationCount structure change.

The change from direct property access to array access suggests a data structure change. Ensure that all existing workstationCount values have been migrated to the new array format with 'intern' key.

zmsentities/tests/Zmsentities/AvailabilityTest.php (5)

199-199: LGTM! Consistent with previous change.

The update to 6534000 seconds is consistent with the change in testGetAvailableSecondsPerDay(), maintaining test consistency.


Line range hint 870-957: LGTM! Improved conflict detection test coverage.

The changes enhance the conflict detection tests with:

  1. More comprehensive test cases for overlapping availabilities
  2. Clear and descriptive error messages including specific dates and times
  3. Additional assertions for exact matches and slot size conflicts
  4. Improved structure for conflict list handling

These improvements will help ensure robust validation of availability conflicts.


501-501: Verify the increased public workstation count.

The expected public workstation count has been increased from 81 to 99. Please confirm this significant increase aligns with the capacity planning requirements.

✅ Verification successful

Test assertion for calculated workstation count is valid

The change from 81 to 99 workstations is an intentional update to the test case that verifies the withCalculatedSlots() calculation logic. This is a unit test asserting the expected behavior, not a production capacity configuration.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for related workstation count calculations

# Look for other tests or implementations using workstation counts
rg -A 5 "workstationCount.*public.*[89][19]"

# Look for the implementation of withCalculatedSlots
ast-grep --pattern 'function withCalculatedSlots() {
  $$$
}'

Length of output: 110


Script:

#!/bin/bash
# Search for workstation count references and calculations
rg -l "workstationCount.*public"

# Get the content of the test file to understand the context
cat zmsentities/tests/Zmsentities/AvailabilityTest.php

# Look for other availability or capacity related tests
fd "Test.php" | grep -i "availability\|capacity"

Length of output: 41271


510-512: Verify the updated slot calculations.

The changes include:

  1. Increased total slot count from 28 to 33
  2. Changed second slot time from '10:12' to '10:10'

Please confirm these updates align with the intended scheduling intervals and operating hours.

✅ Verification successful

Slot calculation changes are verified and consistent

The modifications align with the system's design:

  • 10-minute slot intervals are confirmed across multiple test files
  • The increase to 33 slots maintains the consistent 10-minute interval pattern
  • The second slot timing (10:10) correctly follows the first slot (10:00)
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for related slot calculations

# Look for slot time calculations
ast-grep --pattern 'function getSlotList() {
  $$$
}'

# Look for other tests using these specific times
rg -A 5 "10:10|10:12"

Length of output: 5421


Script:

#!/bin/bash
# Search for slot-related implementations and configurations
echo "=== Searching for slot-related files ==="
fd -t f -e php slot

echo -e "\n=== Searching for slot calculations and configurations ==="
rg -l "slot|interval|duration" --type php

echo -e "\n=== Looking for related test assertions ==="
rg "assertEquals.*slot|assertTrue.*slot" --type php

Length of output: 11898


182-182: Verify the updated available seconds per day value.

The expected value has been increased from 5346000 to 6534000 seconds (approximately 1.8 hours difference). Please confirm this aligns with the business requirements for daily availability calculations.

Comment on lines +48 to +54
if (!isset($input['availabilityList']) || !is_array($input['availabilityList'])) {
throw new BadRequestException('Missing or invalid availabilityList.');
} else if (!isset($input['availabilityList'][0]['scope'])) {
throw new BadRequestException('Missing or invalid scope.');
} else if (!isset($input['selectedDate'])) {
throw new BadRequestException("'selectedDate' is required.");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Duplicate input validation logic.

This validation logic is duplicated across multiple classes (AvailabilityAdd, AvailabilityUpdate, AvailabilityConflicts).

Extract the validation logic into a shared trait:

trait AvailabilityValidationTrait {
    protected function validateAvailabilityInput(array $input): void {
        if (!isset($input['availabilityList']) || !is_array($input['availabilityList'])) {
            throw new BadRequestException('Missing or invalid availabilityList.');
        } else if (!isset($input['availabilityList'][0]['scope'])) {
            throw new BadRequestException('Missing or invalid scope.');
        } else if (!isset($input['selectedDate'])) {
            throw new BadRequestException("'selectedDate' is required.");
        }
    }
}

Comment on lines 175 to 202
protected function checkNewVsNewConflicts(Collection $collection, \DateTimeImmutable $selectedDate): void
{
foreach ($collection as $availability1) {
foreach ($collection as $availability2) {
$scope1Id = is_array($availability1->scope) ? ($availability1->scope['id'] ?? null) : ($availability1->scope->id ?? null);
$scope2Id = is_array($availability2->scope) ? ($availability2->scope['id'] ?? null) : ($availability2->scope->id ?? null);

if ($availability1 !== $availability2 &&
$availability1->type == $availability2->type &&
$scope1Id == $scope2Id &&
$availability1->hasSharedWeekdayWith($availability2)) {

$start1 = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)
->modify($availability1->startTime);
$end1 = (new \DateTimeImmutable())->setTimestamp($availability1->endDate)
->modify($availability1->endTime);
$start2 = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)
->modify($availability2->startTime);
$end2 = (new \DateTimeImmutable())->setTimestamp($availability2->endDate)
->modify($availability2->endTime);

if ($start1 < $end2 && $start2 < $end1) {
throw new AvailabilityAddFailed('Neue Öffnungszeiten überschneiden sich.');
}
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Duplicate conflict checking logic.

The checkNewVsNewConflicts method is duplicated across AvailabilityAdd and AvailabilityUpdate classes.

Extract this logic into a shared trait or base class:

trait AvailabilityConflictTrait {
    protected function checkNewVsNewConflicts(Collection $collection, \DateTimeImmutable $selectedDate): void {
        // Move the entire checkNewVsNewConflicts method here
    }
}

@ThomasAFink
Copy link
Member Author

@coderabbitai when two new opening hours are overlapping I only get

Folgende Zeiten führen mit der aktuellen Auswahl zu Konflikten:

but I should be getting which ones like when comparing a new one to an existing one.

Folgende Zeiten führen mit der aktuellen Auswahl zu Konflikten:
Konflikt: Zwei Öffnungszeiten überschneiden sich.
Bestehende Öffnungszeit:  [30.01.2025 - 30.01.2025, 07:00 - 08:00]
Neue Öffnungszeit:                 [30.01.2025 - 30.01.2025, 07:00 - 09:00]

Copy link
Contributor

coderabbitai bot commented Jan 29, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fix for the checkNewVsNewConflicts method in AvailabilityConflicts:

protected static function checkNewVsNewConflicts(AvailabilityList $collection, \DateTimeImmutable $selectedDateTime)
{
    $conflicts = new \BO\Zmsentities\Collection\ProcessList();
    
    foreach ($collection as $availability1) {
        foreach ($collection as $availability2) {
            $scope1Id = is_array($availability1->scope) ? ($availability1->scope['id'] ?? null) : ($availability1->scope->id ?? null);
            $scope2Id = is_array($availability2->scope) ? ($availability2->scope['id'] ?? null) : ($availability2->scope->id ?? null);
            
            if ($availability1 !== $availability2 && 
                $availability1->type == $availability2->type &&
                $scope1Id == $scope2Id &&
                $availability1->hasSharedWeekdayWith($availability2)) {
                
                // First check if dates are the same
                $date1 = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)->format('Y-m-d');
                $date2 = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)->format('Y-m-d');
                
                if ($date1 === $date2) {
                    // Compare times as strings for exact boundary handling
                    $time1Start = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)
                        ->modify($availability1->startTime)->format('H:i');
                    $time1End = (new \DateTimeImmutable())->setTimestamp($availability1->endDate)
                        ->modify($availability1->endTime)->format('H:i');
                    $time2Start = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)
                        ->modify($availability2->startTime)->format('H:i');
                    $time2End = (new \DateTimeImmutable())->setTimestamp($availability2->endDate)
                        ->modify($availability2->endTime)->format('H:i');

                    if ($time2Start < $time1End && $time1Start < $time2End) {
                        $process = new Process();
                        
                        $dateRange1 = date('d.m.Y', $availability1->startDate) . ' - ' . date('d.m.Y', $availability1->endDate);
                        $dateRange2 = date('d.m.Y', $availability2->startDate) . ' - ' . date('d.m.Y', $availability2->endDate);
                        $timeRange1 = $time1Start . ' - ' . $time1End;
                        $timeRange2 = $time2Start . ' - ' . $time2End;

                        $process->amendment = "Konflikt: Zwei Öffnungszeiten überschneiden sich.\n"
                            . "Bestehende Öffnungszeit:&thinsp;&thinsp;[$dateRange1, $timeRange1]\n"
                            . "Neue Öffnungszeit:&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;[$dateRange2, $timeRange2]";
                        
                        $appointment = new \BO\Zmsentities\Appointment();
                        $appointment->date = $availability1->startDate;
                        $appointment->availability = $availability1;
                        $process->addAppointment($appointment);
                        $conflicts->addEntity($process);
                    }
                }
            }
        }
    }
    
    return $conflicts;
}

Key changes:

  1. Added date range and time range formatting
  2. Used the same message format as getTimeOverlaps
  3. Includes both availabilities' details in the error message

✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/803

Timestamp: 2025-01-29T14:20:23.456Z
Learning: In the eappointment system's availability validation, error messages for overlapping availabilities should include detailed information about both availabilities (date ranges and time ranges) in a consistent format, whether checking new vs new or new vs existing conflicts.
🔗 Analysis chain
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Find and show the implementation of overlap checks
echo "=== Content of AvailabilityConflicts.php ==="
cat zmsadmin/src/Zmsadmin/AvailabilityConflicts.php

echo -e "\n=== Content of Availability.php ==="
cat zmsentities/src/Zmsentities/Availability.php

echo -e "\n=== Looking for conflict message formatting ==="
rg -l "amendment.*Konflikt" --type php

Length of output: 42182

@ThomasAFink
Copy link
Member Author

ThomasAFink commented Jan 29, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fix for the checkNewVsNewConflicts method in AvailabilityConflicts:

protected static function checkNewVsNewConflicts(AvailabilityList $collection, \DateTimeImmutable $selectedDateTime)
{
    $conflicts = new \BO\Zmsentities\Collection\ProcessList();
    
    foreach ($collection as $availability1) {
        foreach ($collection as $availability2) {
            $scope1Id = is_array($availability1->scope) ? ($availability1->scope['id'] ?? null) : ($availability1->scope->id ?? null);
            $scope2Id = is_array($availability2->scope) ? ($availability2->scope['id'] ?? null) : ($availability2->scope->id ?? null);
            
            if ($availability1 !== $availability2 && 
                $availability1->type == $availability2->type &&
                $scope1Id == $scope2Id &&
                $availability1->hasSharedWeekdayWith($availability2)) {
                
                // First check if dates are the same
                $date1 = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)->format('Y-m-d');
                $date2 = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)->format('Y-m-d');
                
                if ($date1 === $date2) {
                    // Compare times as strings for exact boundary handling
                    $time1Start = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)
                        ->modify($availability1->startTime)->format('H:i');
                    $time1End = (new \DateTimeImmutable())->setTimestamp($availability1->endDate)
                        ->modify($availability1->endTime)->format('H:i');
                    $time2Start = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)
                        ->modify($availability2->startTime)->format('H:i');
                    $time2End = (new \DateTimeImmutable())->setTimestamp($availability2->endDate)
                        ->modify($availability2->endTime)->format('H:i');

                    if ($time2Start < $time1End && $time1Start < $time2End) {
                        $process = new Process();
                        
                        $dateRange1 = date('d.m.Y', $availability1->startDate) . ' - ' . date('d.m.Y', $availability1->endDate);
                        $dateRange2 = date('d.m.Y', $availability2->startDate) . ' - ' . date('d.m.Y', $availability2->endDate);
                        $timeRange1 = $time1Start . ' - ' . $time1End;
                        $timeRange2 = $time2Start . ' - ' . $time2End;

                        $process->amendment = "Konflikt: Zwei Öffnungszeiten überschneiden sich.\n"
                            . "Bestehende Öffnungszeit:&thinsp;&thinsp;[$dateRange1, $timeRange1]\n"
                            . "Neue Öffnungszeit:&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;[$dateRange2, $timeRange2]";
                        
                        $appointment = new \BO\Zmsentities\Appointment();
                        $appointment->date = $availability1->startDate;
                        $appointment->availability = $availability1;
                        $process->addAppointment($appointment);
                        $conflicts->addEntity($process);
                    }
                }
            }
        }
    }
    
    return $conflicts;
}

Key changes:

  1. Added date range and time range formatting
  2. Used the same message format as getTimeOverlaps
  3. Includes both availabilities' details in the error message

✏️ Learnings added
🔗 Analysis chain
🏁 Scripts executed

@coderabbitai now when I compare new to existing I get double:

Folgende Zeiten führen mit der aktuellen Auswahl zu Konflikten:
Konflikt: Zwei Öffnungszeiten überschneiden sich.
Bestehende Öffnungszeit:  [30.01.2025 - 30.01.2025, 07:00 - 09:00]
Neue Öffnungszeit:                 [30.01.2025 - 30.01.2025, 07:00 - 08:00]
Konflikt: Zwei Öffnungszeiten überschneiden sich.
Bestehende Öffnungszeit:  [30.01.2025 - 30.01.2025, 07:00 - 08:00]
Neue Öffnungszeit:                 [30.01.2025 - 30.01.2025, 07:00 - 09:00]

shouldn't checkNewVsNewConflicts only be checking new against new?

Copy link
Contributor

coderabbitai bot commented Jan 29, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fix to prevent duplicate conflict messages:

protected static function checkNewVsNewConflicts(AvailabilityList $collection, \DateTimeImmutable $selectedDateTime)
{
    $conflicts = new \BO\Zmsentities\Collection\ProcessList();
    $processedPairs = [];
    
    foreach ($collection as $availability1) {
        foreach ($collection as $availability2) {
            // Create a unique key for this pair (sort IDs to ensure same key regardless of order)
            $id1 = $availability1->id ?? $availability1->tempId;
            $id2 = $availability2->id ?? $availability2->tempId;
            $pairKey = $id1 < $id2 ? "$id1-$id2" : "$id2-$id1";
            
            // Skip if we've already processed this pair or if it's the same availability
            if ($availability1 === $availability2 || isset($processedPairs[$pairKey])) {
                continue;
            }
            $processedPairs[$pairKey] = true;
            
            $scope1Id = is_array($availability1->scope) ? ($availability1->scope['id'] ?? null) : ($availability1->scope->id ?? null);
            $scope2Id = is_array($availability2->scope) ? ($availability2->scope['id'] ?? null) : ($availability2->scope->id ?? null);
            
            if ($availability1->type == $availability2->type &&
                $scope1Id == $scope2Id &&
                $availability1->hasSharedWeekdayWith($availability2)) {
                
                // First check if dates are the same
                $date1 = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)->format('Y-m-d');
                $date2 = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)->format('Y-m-d');
                
                if ($date1 === $date2) {
                    // Compare times as strings for exact boundary handling
                    $time1Start = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)
                        ->modify($availability1->startTime)->format('H:i');
                    $time1End = (new \DateTimeImmutable())->setTimestamp($availability1->endDate)
                        ->modify($availability1->endTime)->format('H:i');
                    $time2Start = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)
                        ->modify($availability2->startTime)->format('H:i');
                    $time2End = (new \DateTimeImmutable())->setTimestamp($availability2->endDate)
                        ->modify($availability2->endTime)->format('H:i');

                    if ($time2Start < $time1End && $time1Start < $time2End) {
                        $process = new Process();
                        
                        $dateRange1 = date('d.m.Y', $availability1->startDate) . ' - ' . date('d.m.Y', $availability1->endDate);
                        $dateRange2 = date('d.m.Y', $availability2->startDate) . ' - ' . date('d.m.Y', $availability2->endDate);
                        $timeRange1 = $time1Start . ' - ' . $time1End;
                        $timeRange2 = $time2Start . ' - ' . $time2End;

                        $process->amendment = "Konflikt: Zwei Öffnungszeiten überschneiden sich.\n"
                            . "Bestehende Öffnungszeit:&thinsp;&thinsp;[$dateRange1, $timeRange1]\n"
                            . "Neue Öffnungszeit:&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;[$dateRange2, $timeRange2]";
                        
                        $appointment = new \BO\Zmsentities\Appointment();
                        $appointment->date = $availability1->startDate;
                        $appointment->availability = $availability1;
                        $process->addAppointment($appointment);
                        $conflicts->addEntity($process);
                    }
                }
            }
        }
    }
    
    return $conflicts;
}

Key changes:

  1. Added a $processedPairs array to track which pairs have been checked
  2. Create a unique key for each pair that's the same regardless of order
  3. Skip pairs we've already processed
  4. This ensures each overlap is only reported once

✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/803

Timestamp: 2025-01-29T14:29:37.273Z
Learning: In the eappointment system's availability validation, when checking for overlaps between multiple availabilities, use a tracking mechanism like processedPairs to ensure each pair is only checked once, preventing duplicate conflict messages.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Contributor

coderabbitai bot commented Jan 29, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink You're absolutely right. Here's the corrected version that only checks new availabilities against each other:

protected static function checkNewVsNewConflicts(AvailabilityList $collection, \DateTimeImmutable $selectedDateTime)
{
    $conflicts = new \BO\Zmsentities\Collection\ProcessList();
    
    // Filter to only get new availabilities
    $newAvailabilities = new AvailabilityList();
    foreach ($collection as $availability) {
        if (isset($availability->tempId) && strpos($availability->tempId, '__temp__') !== false) {
            $newAvailabilities->addEntity($availability);
        }
    }
    
    foreach ($newAvailabilities as $availability1) {
        foreach ($newAvailabilities as $availability2) {
            $scope1Id = is_array($availability1->scope) ? ($availability1->scope['id'] ?? null) : ($availability1->scope->id ?? null);
            $scope2Id = is_array($availability2->scope) ? ($availability2->scope['id'] ?? null) : ($availability2->scope->id ?? null);
            
            if ($availability1 !== $availability2 && 
                $availability1->type == $availability2->type &&
                $scope1Id == $scope2Id &&
                $availability1->hasSharedWeekdayWith($availability2)) {
                
                // First check if dates are the same
                $date1 = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)->format('Y-m-d');
                $date2 = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)->format('Y-m-d');
                
                if ($date1 === $date2) {
                    // Compare times as strings for exact boundary handling
                    $time1Start = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)
                        ->modify($availability1->startTime)->format('H:i');
                    $time1End = (new \DateTimeImmutable())->setTimestamp($availability1->endDate)
                        ->modify($availability1->endTime)->format('H:i');
                    $time2Start = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)
                        ->modify($availability2->startTime)->format('H:i');
                    $time2End = (new \DateTimeImmutable())->setTimestamp($availability2->endDate)
                        ->modify($availability2->endTime)->format('H:i');

                    if ($time2Start < $time1End && $time1Start < $time2End) {
                        $process = new Process();
                        
                        $dateRange1 = date('d.m.Y', $availability1->startDate) . ' - ' . date('d.m.Y', $availability1->endDate);
                        $dateRange2 = date('d.m.Y', $availability2->startDate) . ' - ' . date('d.m.Y', $availability2->endDate);
                        $timeRange1 = $time1Start . ' - ' . $time1End;
                        $timeRange2 = $time2Start . ' - ' . $time2End;

                        $process->amendment = "Konflikt: Zwei Öffnungszeiten überschneiden sich.\n"
                            . "Bestehende Öffnungszeit:&thinsp;&thinsp;[$dateRange1, $timeRange1]\n"
                            . "Neue Öffnungszeit:&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;[$dateRange2, $timeRange2]";
                        
                        $appointment = new \BO\Zmsentities\Appointment();
                        $appointment->date = $availability1->startDate;
                        $appointment->availability = $availability1;
                        $process->addAppointment($appointment);
                        $conflicts->addEntity($process);
                    }
                }
            }
        }
    }
    
    return $conflicts;
}

Key changes:

  1. Added filtering at the start to create a collection of only new availabilities
  2. Perform overlap checks only on the filtered collection
  3. This ensures we only check new availabilities against each other

✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/803

Timestamp: 2025-01-29T14:33:18.905Z
Learning: In the eappointment system's availability validation, when checking for overlaps between new availabilities (checkNewVsNewConflicts), first filter the collection to only include availabilities with tempId containing '__temp__' to ensure only new availabilities are compared against each other.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@ThomasAFink
Copy link
Member Author

checkNewVsNewConflicts

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink You're absolutely right. Here's the corrected version that only checks new availabilities against each other:

protected static function checkNewVsNewConflicts(AvailabilityList $collection, \DateTimeImmutable $selectedDateTime)
{
    $conflicts = new \BO\Zmsentities\Collection\ProcessList();
    
    // Filter to only get new availabilities
    $newAvailabilities = new AvailabilityList();
    foreach ($collection as $availability) {
        if (isset($availability->tempId) && strpos($availability->tempId, '__temp__') !== false) {
            $newAvailabilities->addEntity($availability);
        }
    }
    
    foreach ($newAvailabilities as $availability1) {
        foreach ($newAvailabilities as $availability2) {
            $scope1Id = is_array($availability1->scope) ? ($availability1->scope['id'] ?? null) : ($availability1->scope->id ?? null);
            $scope2Id = is_array($availability2->scope) ? ($availability2->scope['id'] ?? null) : ($availability2->scope->id ?? null);
            
            if ($availability1 !== $availability2 && 
                $availability1->type == $availability2->type &&
                $scope1Id == $scope2Id &&
                $availability1->hasSharedWeekdayWith($availability2)) {
                
                // First check if dates are the same
                $date1 = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)->format('Y-m-d');
                $date2 = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)->format('Y-m-d');
                
                if ($date1 === $date2) {
                    // Compare times as strings for exact boundary handling
                    $time1Start = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)
                        ->modify($availability1->startTime)->format('H:i');
                    $time1End = (new \DateTimeImmutable())->setTimestamp($availability1->endDate)
                        ->modify($availability1->endTime)->format('H:i');
                    $time2Start = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)
                        ->modify($availability2->startTime)->format('H:i');
                    $time2End = (new \DateTimeImmutable())->setTimestamp($availability2->endDate)
                        ->modify($availability2->endTime)->format('H:i');

                    if ($time2Start < $time1End && $time1Start < $time2End) {
                        $process = new Process();
                        
                        $dateRange1 = date('d.m.Y', $availability1->startDate) . ' - ' . date('d.m.Y', $availability1->endDate);
                        $dateRange2 = date('d.m.Y', $availability2->startDate) . ' - ' . date('d.m.Y', $availability2->endDate);
                        $timeRange1 = $time1Start . ' - ' . $time1End;
                        $timeRange2 = $time2Start . ' - ' . $time2End;

                        $process->amendment = "Konflikt: Zwei Öffnungszeiten überschneiden sich.\n"
                            . "Bestehende Öffnungszeit:&thinsp;&thinsp;[$dateRange1, $timeRange1]\n"
                            . "Neue Öffnungszeit:&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;[$dateRange2, $timeRange2]";
                        
                        $appointment = new \BO\Zmsentities\Appointment();
                        $appointment->date = $availability1->startDate;
                        $appointment->availability = $availability1;
                        $process->addAppointment($appointment);
                        $conflicts->addEntity($process);
                    }
                }
            }
        }
    }
    
    return $conflicts;
}

Key changes:

  1. Added filtering at the start to create a collection of only new availabilities
  2. Perform overlap checks only on the filtered collection
  3. This ensures we only check new availabilities against each other

✏️ Learnings added

@coderabbitai does checkNewVsNewConflicts in AvailabilityAdd and AvailabilityUpdate also only check new ones? Can you double check?

Copy link
Contributor

coderabbitai bot commented Jan 29, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink No, the checkNewVsNewConflicts methods in both AvailabilityAdd and AvailabilityUpdate are not properly filtering for new availabilities. They're currently checking all availabilities in the collection against each other. Here's how they should be modified to match the AvailabilityConflicts implementation:

protected function checkNewVsNewConflicts(Collection $collection, \DateTimeImmutable $selectedDate): void
{
    // Filter to only get new availabilities
    $newAvailabilities = new Collection();
    foreach ($collection as $availability) {
        if (isset($availability->tempId) && strpos($availability->tempId, '__temp__') !== false) {
            $newAvailabilities->addEntity($availability);
        }
    }
    
    foreach ($newAvailabilities as $availability1) {
        foreach ($newAvailabilities as $availability2) {
            $scope1Id = is_array($availability1->scope) ? ($availability1->scope['id'] ?? null) : ($availability1->scope->id ?? null);
            $scope2Id = is_array($availability2->scope) ? ($availability2->scope['id'] ?? null) : ($availability2->scope->id ?? null);
            
            if ($availability1 !== $availability2 && 
                $availability1->type == $availability2->type &&
                $scope1Id == $scope2Id &&
                $availability1->hasSharedWeekdayWith($availability2)) {
                
                // First check if dates are the same
                $date1 = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)->format('Y-m-d');
                $date2 = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)->format('Y-m-d');
                
                if ($date1 === $date2) {
                    $time1Start = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)
                        ->modify($availability1->startTime)->format('H:i');
                    $time1End = (new \DateTimeImmutable())->setTimestamp($availability1->endDate)
                        ->modify($availability1->endTime)->format('H:i');
                    $time2Start = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)
                        ->modify($availability2->startTime)->format('H:i');
                    $time2End = (new \DateTimeImmutable())->setTimestamp($availability2->endDate)
                        ->modify($availability2->endTime)->format('H:i');

                    if ($time2Start < $time1End && $time1Start < $time2End) {
                        throw new AvailabilityAddFailed('Neue Öffnungszeiten überschneiden sich.'); // or AvailabilityUpdateFailed
                    }
                }
            }
        }
    }
}

This change will ensure that only new availabilities (those with tempId containing 'temp') are checked against each other.


✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/803

Timestamp: 2025-01-29T14:40:23.456Z
Learning: In the eappointment system's AvailabilityAdd and AvailabilityUpdate controllers, the checkNewVsNewConflicts method should filter for availabilities with tempId containing '__temp__' before checking for overlaps, to ensure only new availabilities are compared against each other.
🔗 Analysis chain
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Find and show checkNewVsNewConflicts implementations
echo "=== Content of AvailabilityAdd.php ==="
cat zmsapi/src/Zmsapi/AvailabilityAdd.php

echo -e "\n=== Content of AvailabilityUpdate.php ==="
cat zmsapi/src/Zmsapi/AvailabilityUpdate.php

Length of output: 17865


🧠 Learnings used
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-01-29T14:33:19.112Z
Learning: In the eappointment system's availability validation, when checking for overlaps between new availabilities (checkNewVsNewConflicts), first filter the collection to only include availabilities with tempId containing '__temp__' to ensure only new availabilities are compared against each other.

Copy link
Contributor

@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: 1

♻️ Duplicate comments (2)
zmsapi/src/Zmsapi/AvailabilityUpdate.php (2)

145-166: 🛠️ Refactor suggestion

Add transaction management for entity updates.

The entity update logic should be wrapped in a transaction to ensure data consistency, especially when handling multiple entities.


104-105: 🛠️ Refactor suggestion

Add logging for validation failures and conflicts.

Important error conditions should be logged for debugging and monitoring.

Also applies to: 128-129

🧹 Nitpick comments (2)
zmsadmin/src/Zmsadmin/AvailabilityConflicts.php (1)

61-75: Consider optimizing the filtering logic to reduce iterations.

The current implementation requires multiple iterations over the collection. Consider combining the origin ID search and filtering into a single pass.

-        $originId = null;
-        foreach ($availabilityList as $availability) {
-            if (isset($availability->kind) && $availability->kind === 'origin' && isset($availability->id)) {
-                $originId = $availability->id;
-                break;
-            }
-        }
-    
-        $filteredAvailabilityList = new AvailabilityList();
-        foreach ($availabilityList as $availability) {
-            if ((!isset($availability->kind) || $availability->kind !== 'exclusion') && 
-                (!isset($availability->id) || $availability->id !== $originId)) {
-                $filteredAvailabilityList->addEntity($availability);
-            }
-        }
+        $originId = null;
+        $filteredAvailabilityList = new AvailabilityList();
+        foreach ($availabilityList as $availability) {
+            if (isset($availability->kind) && $availability->kind === 'origin' && isset($availability->id)) {
+                $originId = $availability->id;
+            }
+            if ((!isset($availability->kind) || $availability->kind !== 'exclusion') && 
+                (!isset($availability->id) || $availability->id !== $originId)) {
+                $filteredAvailabilityList->addEntity($availability);
+            }
+        }
zmsapi/src/Zmsapi/AvailabilityUpdate.php (1)

43-55: Consider extracting shared validation logic.

While the validation is thorough, this logic is duplicated with AvailabilityConflicts.php. Consider extracting it into a shared trait or base class.

trait AvailabilityValidationTrait {
    protected function validateInput(array $input): void {
        if (!$input || count($input) === 0) {
            throw new BadRequestException();
        }
        if (!isset($input['availabilityList']) || !is_array($input['availabilityList'])) {
            throw new BadRequestException('Missing or invalid availabilityList.');
        }
        // ... rest of validation logic
    }
}
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 860abc6 and 3a3491f.

📒 Files selected for processing (3)
  • zmsadmin/src/Zmsadmin/AvailabilityConflicts.php (4 hunks)
  • zmsapi/src/Zmsapi/AvailabilityAdd.php (3 hunks)
  • zmsapi/src/Zmsapi/AvailabilityUpdate.php (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • zmsapi/src/Zmsapi/AvailabilityAdd.php
⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: call-build-images / build (zmsticketprinter, 8.0)
  • GitHub Check: call-build-images / build (zmsstatistic, 8.0)
  • GitHub Check: call-build-images / build (zmsmessaging, 8.0)
  • GitHub Check: call-build-images / build (zmscitizenapi, 8.0)
  • GitHub Check: call-build-images / build (zmscalldisplay, 8.0)
  • GitHub Check: call-build-images / build (zmsapi, 8.0)
  • GitHub Check: call-build-images / build (zmsadmin, 8.0)
🔇 Additional comments (3)
zmsadmin/src/Zmsadmin/AvailabilityConflicts.php (3)

35-41: LGTM! Input validation is thorough and well-structured.

The validation checks are comprehensive and follow a logical order, with clear error messages for each failure case.


165-171: LGTM! Documentation is clear and follows PHPDoc standards.

The method documentation properly describes parameters and return types.


105-163: Consider optimizing conflict detection performance.

The nested loops in the conflict detection could become a performance bottleneck with a large number of availabilities. Consider:

  1. Using a time-based indexing structure
  2. Early exit when conflicts are found
  3. Reducing the number of DateTime object creations

Also, verify edge cases:

  • Availabilities spanning midnight
  • DST transitions
  • Timezone differences

Comment on lines +182 to +216
protected function checkNewVsNewConflicts(Collection $collection, \DateTimeImmutable $selectedDate): void
{
$newAvailabilities = new Collection();
foreach ($collection as $availability) {
if (isset($availability->tempId) && strpos($availability->tempId, '__temp__') !== false) {
$newAvailabilities->addEntity($availability);
}
}

foreach ($collection as $availability1) {
foreach ($collection as $availability2) {
$scope1Id = is_array($availability1->scope) ? ($availability1->scope['id'] ?? null) : ($availability1->scope->id ?? null);
$scope2Id = is_array($availability2->scope) ? ($availability2->scope['id'] ?? null) : ($availability2->scope->id ?? null);

if ($availability1 !== $availability2 &&
$availability1->type == $availability2->type &&
$scope1Id == $scope2Id &&
$availability1->hasSharedWeekdayWith($availability2)) {

$start1 = (new \DateTimeImmutable())->setTimestamp($availability1->startDate)
->modify($availability1->startTime);
$end1 = (new \DateTimeImmutable())->setTimestamp($availability1->endDate)
->modify($availability1->endTime);
$start2 = (new \DateTimeImmutable())->setTimestamp($availability2->startDate)
->modify($availability2->startTime);
$end2 = (new \DateTimeImmutable())->setTimestamp($availability2->endDate)
->modify($availability2->endTime);

if ($start1 < $end2 && $start2 < $end1) {
throw new AvailabilityUpdateFailed('Neue Öffnungszeiten überschneiden sich.');
}
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consolidate conflict checking logic across classes.

The conflict checking logic is similar to AvailabilityConflicts.php but with different implementations. Consider:

  1. Extracting shared logic to reduce duplication
  2. Standardizing error messages across both classes
  3. Using a common interface for conflict detection
interface ConflictDetectorInterface {
    public function checkNewVsNewConflicts(Collection $collection, \DateTimeImmutable $selectedDate): void;
}

trait ConflictDetectorTrait {
    public function checkNewVsNewConflicts(Collection $collection, \DateTimeImmutable $selectedDate): void {
        // Shared implementation
    }
}

@ThomasAFink
Copy link
Member Author

@coderabbitai can we have <button title="Alle Änderungen werden zurückgesetzt" className="button btn" type="abort" onClick={onAbort} disabled={(!stateChanged && !hasConflicts && !data)}>Abbrechen</button> in FooterButtons only disabled when nothing has changed. So existing Availabilities haven't been edited a new ones haven't been created.

Copy link
Contributor

coderabbitai bot commented Jan 29, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the corrected FooterButtons component with the simplified disabled condition for the Abort button:

const FooterButtons = (props) => {
    const { hasConflicts, stateChanged, data, onNew, onPublish, onAbort, hasSlotCountError } = props
    return (
        <div className="form-actions" style={{ "marginTop": "0", "padding": "0.75em" }}>
            <button title="Neue Öffnungszeit anlegen und bearbeiten" className="button button--diamond button-new" onClick={onNew} disabled={(stateChanged || hasConflicts || data)}>neue Öffnungszeit</button>
            <button title="Alle Änderungen werden zurückgesetzt" className="button btn" type="abort" onClick={onAbort} disabled={!stateChanged}>Abbrechen</button>
            <button title="Alle Änderungen werden gespeichert" className="button button--positive button-save" type="save" value="publish" onClick={onPublish} disabled={(!stateChanged || hasSlotCountError || hasConflicts)}>Alle Änderungen aktivieren
            </button>
        </div>
    )
}

Key change:

  • Changed the Abort button's disabled condition to just !stateChanged since stateChanged is set to true whenever:
    • Existing availabilities are modified
    • New availabilities are created
    • Any changes are made to the state

This will keep the button enabled as long as there are unsaved changes in the system.


✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/803

Timestamp: 2025-01-29T14:45:23.456Z
Learning: In the eappointment system's FooterButtons component, the Abort button should be enabled whenever there are unsaved changes (stateChanged is true), regardless of conflicts or selected data, to allow users to reset their changes at any time.
🔗 Analysis chain
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Find and show the FooterButtons component implementation
echo "=== Content of FooterButtons component ==="
cat zmsadmin/js/page/availabilityDay/form/footerButtons.js

echo -e "\n=== Looking for state management ==="
rg "stateChanged" --type js --glob "zmsadmin/js/page/availabilityDay/**"

Length of output: 3638

@ThomasAFink
Copy link
Member Author

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the corrected FooterButtons component with the simplified disabled condition for the Abort button:

const FooterButtons = (props) => {
    const { hasConflicts, stateChanged, data, onNew, onPublish, onAbort, hasSlotCountError } = props
    return (
        <div className="form-actions" style={{ "marginTop": "0", "padding": "0.75em" }}>
            <button title="Neue Öffnungszeit anlegen und bearbeiten" className="button button--diamond button-new" onClick={onNew} disabled={(stateChanged || hasConflicts || data)}>neue Öffnungszeit</button>
            <button title="Alle Änderungen werden zurückgesetzt" className="button btn" type="abort" onClick={onAbort} disabled={!stateChanged}>Abbrechen</button>
            <button title="Alle Änderungen werden gespeichert" className="button button--positive button-save" type="save" value="publish" onClick={onPublish} disabled={(!stateChanged || hasSlotCountError || hasConflicts)}>Alle Änderungen aktivieren
            </button>
        </div>
    )
}

Key change:

  • Changed the Abort button's disabled condition to just !stateChanged since stateChanged is set to true whenever:

    • Existing availabilities are modified
    • New availabilities are created
    • Any changes are made to the state

This will keep the button enabled as long as there are unsaved changes in the system.

✏️ Learnings added
🔗 Analysis chain
🏁 Scripts executed

@coderabbitai stateChanged only tracks if something in one of the forms has changed it doesn't track if I add a new availability. So if I add a new availability and haven'T changed any of the forms stateChanged is still false and therefore the button remains disabled.

Copy link
Contributor

coderabbitai bot commented Jan 29, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fixed FooterButtons component:

const FooterButtons = (props) => {
    const { hasConflicts, stateChanged, data, onNew, onPublish, onAbort, hasSlotCountError, availabilitylist } = props;
    
    // Check if there are any new availabilities (with tempId)
    const hasNewAvailabilities = availabilitylist && availabilitylist.some(
        availability => availability.tempId && availability.tempId.includes('__temp__')
    );
    
    return (
        <div className="form-actions" style={{ "marginTop": "0", "padding": "0.75em" }}>
            <button 
                title="Neue Öffnungszeit anlegen und bearbeiten" 
                className="button button--diamond button-new" 
                onClick={onNew} 
                disabled={(stateChanged || hasConflicts || data)}
            >
                neue Öffnungszeit
            </button>
            <button 
                title="Alle Änderungen werden zurückgesetzt" 
                className="button btn" 
                type="abort" 
                onClick={onAbort} 
                disabled={!stateChanged && !hasNewAvailabilities && !hasConflicts}
            >
                Abbrechen
            </button>
            <button 
                title="Alle Änderungen werden gespeichert" 
                className="button button--positive button-save" 
                type="save" 
                value="publish" 
                onClick={onPublish} 
                disabled={(!stateChanged && !hasNewAvailabilities) || hasSlotCountError || hasConflicts}
            >
                Alle Änderungen aktivieren
            </button>
        </div>
    )
}

FooterButtons.propTypes = {
    data: PropTypes.object,
    hasConflicts: PropTypes.bool,
    stateChanged: PropTypes.bool,
    onNew: PropTypes.func,
    onPublish: PropTypes.func,
    onAbort: PropTypes.func,
    hasSlotCountError: PropTypes.bool,
    availabilitylist: PropTypes.array
}

export default FooterButtons

Key changes:

  1. Added availabilitylist to props
  2. Added hasNewAvailabilities check for tempIds containing 'temp'
  3. Updated abort button's disabled condition to consider hasNewAvailabilities
  4. Also updated save button's disabled condition for consistency

✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/803

Timestamp: 2025-01-29T14:50:23.456Z
Learning: In the eappointment system's FooterButtons component, when determining if there are unsaved changes, check both stateChanged (for form changes) and availabilitylist for entries with tempId containing '__temp__' (for new availabilities).
🔗 Analysis chain
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for files handling new availability creation and state management
echo "=== Content of index.js ==="
cat zmsadmin/js/page/availabilityDay/index.js

echo -e "\n=== Content of footerButtons.js ==="
cat zmsadmin/js/page/availabilityDay/form/footerButtons.js

echo -e "\n=== Looking for state management ==="
rg "stateChanged|availabilitylist|newAvailability" --type js --glob "zmsadmin/js/page/availabilityDay/**"

Length of output: 41492

@ThomasAFink
Copy link
Member Author

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fixed FooterButtons component:

const FooterButtons = (props) => {
    const { hasConflicts, stateChanged, data, onNew, onPublish, onAbort, hasSlotCountError, availabilitylist } = props;
    
    // Check if there are any new availabilities (with tempId)
    const hasNewAvailabilities = availabilitylist && availabilitylist.some(
        availability => availability.tempId && availability.tempId.includes('__temp__')
    );
    
    return (
        <div className="form-actions" style={{ "marginTop": "0", "padding": "0.75em" }}>
            <button 
                title="Neue Öffnungszeit anlegen und bearbeiten" 
                className="button button--diamond button-new" 
                onClick={onNew} 
                disabled={(stateChanged || hasConflicts || data)}
            >
                neue Öffnungszeit
            </button>
            <button 
                title="Alle Änderungen werden zurückgesetzt" 
                className="button btn" 
                type="abort" 
                onClick={onAbort} 
                disabled={!stateChanged && !hasNewAvailabilities && !hasConflicts}
            >
                Abbrechen
            </button>
            <button 
                title="Alle Änderungen werden gespeichert" 
                className="button button--positive button-save" 
                type="save" 
                value="publish" 
                onClick={onPublish} 
                disabled={(!stateChanged && !hasNewAvailabilities) || hasSlotCountError || hasConflicts}
            >
                Alle Änderungen aktivieren
            </button>
        </div>
    )
}

FooterButtons.propTypes = {
    data: PropTypes.object,
    hasConflicts: PropTypes.bool,
    stateChanged: PropTypes.bool,
    onNew: PropTypes.func,
    onPublish: PropTypes.func,
    onAbort: PropTypes.func,
    hasSlotCountError: PropTypes.bool,
    availabilitylist: PropTypes.array
}

export default FooterButtons

Key changes:

  1. Added availabilitylist to props
  2. Added hasNewAvailabilities check for tempIds containing 'temp'
  3. Updated abort button's disabled condition to consider hasNewAvailabilities
  4. Also updated save button's disabled condition for consistency

✏️ Learnings added
🔗 Analysis chain
🏁 Scripts executed

@coderabbitai and in here?

        return (
            <Board className="accordion js-accordion"
                title=""
                body={renderAccordionBody()}
                footer={<FooterButtons 
                    hasConflicts={Object.keys(this.props.conflictList.itemList).length || Object.keys(this.props.errorList).length ? true : false}
                    hasSlotCountError={hasSlotCountError(this.props)}
                    stateChanged={this.props.stateChanged} 
                    data={this.props.data}
                    availabilitylist={this.props.availabilityList}
                    {...{onNew, onPublish, onAbort }}
                />}
            />
        )

Copy link
Contributor

coderabbitai bot commented Jan 29, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Yes, there's a prop name mismatch. Here's the corrected FooterButtons component:

const FooterButtons = (props) => {
    const { hasConflicts, stateChanged, data, onNew, onPublish, onAbort, hasSlotCountError, availabilityList } = props;
    
    // Check if there are any new availabilities (with tempId)
    const hasNewAvailabilities = availabilityList && availabilityList.some(
        availability => availability.tempId && availability.tempId.includes('__temp__')
    );
    
    return (
        <div className="form-actions" style={{ "marginTop": "0", "padding": "0.75em" }}>
            <button 
                title="Neue Öffnungszeit anlegen und bearbeiten" 
                className="button button--diamond button-new" 
                onClick={onNew} 
                disabled={(stateChanged || hasConflicts || data)}
            >
                neue Öffnungszeit
            </button>
            <button 
                title="Alle Änderungen werden zurückgesetzt" 
                className="button btn" 
                type="abort" 
                onClick={onAbort} 
                disabled={!stateChanged && !hasNewAvailabilities && !hasConflicts}
            >
                Abbrechen
            </button>
            <button 
                title="Alle Änderungen werden gespeichert" 
                className="button button--positive button-save" 
                type="save" 
                value="publish" 
                onClick={onPublish} 
                disabled={(!stateChanged && !hasNewAvailabilities) || hasSlotCountError || hasConflicts}
            >
                Alle Änderungen aktivieren
            </button>
        </div>
    )
}

FooterButtons.propTypes = {
    data: PropTypes.object,
    hasConflicts: PropTypes.bool,
    stateChanged: PropTypes.bool,
    onNew: PropTypes.func,
    onPublish: PropTypes.func,
    onAbort: PropTypes.func,
    hasSlotCountError: PropTypes.bool,
    availabilityList: PropTypes.array  // Changed from availabilitylist to availabilityList
}

export default FooterButtons

The key change is renaming the prop from availabilitylist to availabilityList to match the camelCase convention used in the Board component and other props.


✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/803

Timestamp: 2025-01-29T15:36:39.026Z
Learning: In the eappointment system's React components, prop names should follow JavaScript camelCase convention for consistency (e.g., availabilityList instead of availabilitylist), especially when the same prop is used across multiple components.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Contributor

@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: 1

🧹 Nitpick comments (4)
zmsadmin/js/page/availabilityDay/layouts/accordion.js (1)

163-165: Maintain consistent prop naming convention.

The prop name availabilitylist uses inconsistent casing compared to the existing availabilityList prop. This could lead to confusion and maintenance issues.

Apply this diff to maintain consistency:

-                    availabilitylist={this.props.availabilityList}
+                    availabilityList={this.props.availabilityList}
zmsadmin/js/page/availabilityDay/form/footerButtons.js (3)

9-9: Extract magic string to a constant.

The string '__temp__' should be extracted to a named constant for better maintainability and reusability.

Add this constant at the top of the file:

+const TEMP_ID_PREFIX = '__temp__';

Then update the check:

-        availability => availability.tempId && availability.tempId.includes('__temp__')
+        availability => availability?.tempId?.includes(TEMP_ID_PREFIX)
🧰 Tools
🪛 Biome (1.9.4)

[error] 9-9: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


27-27: Simplify complex boolean expressions.

The disabled conditions are complex and could be simplified for better readability.

Extract the conditions to descriptive variables:

+    const canAbort = stateChanged || hasNewAvailabilities || hasConflicts;
+    const canSave = (stateChanged || hasNewAvailabilities) && !hasSlotCountError && !hasConflicts;

-                disabled={!stateChanged && !hasNewAvailabilities && !hasConflicts}
+                disabled={!canAbort}

-                disabled={(!stateChanged && !hasNewAvailabilities) || hasSlotCountError || hasConflicts}
+                disabled={!canSave}

Also applies to: 37-37


51-53: Update prop name to maintain consistency.

The prop name should follow the same casing convention as in the parent component.

Apply this diff:

-    availabilitylist: PropTypes.array
+    availabilityList: PropTypes.array
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a3491f and 64c884b.

📒 Files selected for processing (2)
  • zmsadmin/js/page/availabilityDay/form/footerButtons.js (2 hunks)
  • zmsadmin/js/page/availabilityDay/layouts/accordion.js (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
zmsadmin/js/page/availabilityDay/form/footerButtons.js

[error] 8-10: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 9-9: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: call-unit-tests / zmsapi-test
  • GitHub Check: call-unit-tests / zmsdb-test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant