Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions backend/app/DomainObjects/AttendeeCheckInDomainObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ class AttendeeCheckInDomainObject extends Generated\AttendeeCheckInDomainObjectA
{
private ?AttendeeDomainObject $attendee = null;

private ?CheckInListDomainObject $checkInList = null;

public function getAttendee(): ?AttendeeDomainObject
{
return $this->attendee;
Expand All @@ -16,4 +18,15 @@ public function setAttendee(AttendeeDomainObject $attendee): self
$this->attendee = $attendee;
return $this;
}

public function setCheckInList(?CheckInListDomainObject $checkInList): AttendeeCheckInDomainObject
{
$this->checkInList = $checkInList;
return $this;
}

public function getCheckInList(): ?CheckInListDomainObject
{
return $this->checkInList;
}
}
19 changes: 19 additions & 0 deletions backend/app/DomainObjects/AttendeeDomainObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class AttendeeDomainObject extends Generated\AttendeeDomainObjectAbstract implem

public ?AttendeeCheckInDomainObject $checkIn = null;

/** @var Collection<AttendeeCheckInDomainObject>|null */
private ?Collection $checkIns = null;

public static function getDefaultSort(): string
{
return self::CREATED_AT;
Expand Down Expand Up @@ -108,8 +111,24 @@ public function setCheckIn(?AttendeeCheckInDomainObject $checkIn): AttendeeDomai
return $this;
}

/**
* Only use in the context when a single check-in is expected (e.g., when loading a list of attendees for a specific check-in list).
*
* @return AttendeeCheckInDomainObject|null
*/
public function getCheckIn(): ?AttendeeCheckInDomainObject
{
return $this->checkIn;
}

public function setCheckIns(?Collection $checkIns): AttendeeDomainObject
{
$this->checkIns = $checkIns;
return $this;
}

public function getCheckIns(): ?Collection
{
return $this->checkIns;
}
}
18 changes: 12 additions & 6 deletions backend/app/Exports/AttendeesExport.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ public function headings(): array
__('Last Name'),
__('Email'),
__('Status'),
__('Is Checked In'),
__('Checked In At'),
__('Check Ins'),
__('Product ID'),
__('Product Name'),
__('Event ID'),
Expand Down Expand Up @@ -106,16 +105,23 @@ public function map($attendee): array
->getLabel();
}

$checkIns = $attendee->getCheckIns()
? $attendee->getCheckIns()
->map(fn($checkIn) => sprintf(
'%s (%s)',
$checkIn->getCheckInList()?->getName() ?? __('Unknown'),
Carbon::parse($checkIn->getCreatedAt())->format('Y-m-d H:i:s')
))
->join(', ')
: '';

return array_merge([
$attendee->getId(),
$attendee->getFirstName(),
$attendee->getLastName(),
$attendee->getEmail(),
$attendee->getStatus(),
$attendee->getCheckIn() ? 'Yes' : 'No',
$attendee->getCheckIn()
? Carbon::parse($attendee->getCheckIn()->getCreatedAt())->format('Y-m-d H:i:s')
: '',
$checkIns,
$attendee->getProductId(),
$ticketName,
$attendee->getEventId(),
Expand Down
9 changes: 8 additions & 1 deletion backend/app/Http/Actions/Attendees/ExportAttendeesAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace HiEvents\Http\Actions\Attendees;

use HiEvents\DomainObjects\AttendeeCheckInDomainObject;
use HiEvents\DomainObjects\CheckInListDomainObject;
use HiEvents\DomainObjects\Enums\QuestionBelongsTo;
use HiEvents\DomainObjects\EventDomainObject;
use HiEvents\DomainObjects\OrderDomainObject;
Expand Down Expand Up @@ -38,7 +39,13 @@ public function __invoke(int $eventId): BinaryFileResponse
->loadRelation(QuestionAndAnswerViewDomainObject::class)
->loadRelation(new Relationship(
domainObject: AttendeeCheckInDomainObject::class,
name: 'check_in',
nested: [
new Relationship(
domainObject: CheckInListDomainObject::class,
name: 'check_in_list',
),
],
name: 'check_ins',
))
->loadRelation(new Relationship(
domainObject: ProductDomainObject::class,
Expand Down
9 changes: 8 additions & 1 deletion backend/app/Http/Actions/Attendees/GetAttendeeAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace HiEvents\Http\Actions\Attendees;

use HiEvents\DomainObjects\AttendeeCheckInDomainObject;
use HiEvents\DomainObjects\CheckInListDomainObject;
use HiEvents\DomainObjects\EventDomainObject;
use HiEvents\DomainObjects\ProductDomainObject;
use HiEvents\DomainObjects\ProductPriceDomainObject;
Expand Down Expand Up @@ -38,7 +39,13 @@ public function __invoke(int $eventId, int $attendeeId): Response|JsonResponse
], name: 'product'))
->loadRelation(new Relationship(
domainObject: AttendeeCheckInDomainObject::class,
name: 'check_in',
nested: [
new Relationship(
domainObject: CheckInListDomainObject::class,
name: 'check_in_list',
),
],
name: 'check_ins'
))
->findFirstWhere([
'id' => $attendeeId,
Expand Down
1 change: 0 additions & 1 deletion backend/app/Http/Actions/BaseAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Response;
use Spatie\LaravelData\Data;

abstract class BaseAction extends Controller
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ public function __construct(
{
}

public function __invoke(string $uuid, Request $request): JsonResponse
public function __invoke(string $checkInListShortId, Request $request): JsonResponse
{
try {
$attendees = $this->getCheckInListAttendeesPublicHandler->handle(
shortId: $uuid,
shortId: $checkInListShortId,
queryParams: QueryParamsDTO::fromArray($request->query->all())
);
} catch (CannotCheckInException $e) {
Expand Down
4 changes: 2 additions & 2 deletions backend/app/Models/Attendee.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ public function product(): BelongsTo
return $this->belongsTo(Product::class);
}

public function check_in(): HasOne
public function check_ins(): HasMany
{
return $this->hasOne(AttendeeCheckIn::class);
return $this->hasMany(AttendeeCheckIn::class);
}
}
2 changes: 1 addition & 1 deletion backend/app/Models/AttendeeCheckIn.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public function products(): BelongsTo
);
}

public function checkInList(): BelongsTo
public function check_in_list(): BelongsTo
{
return $this->belongsTo(CheckInList::class);
}
Expand Down
2 changes: 1 addition & 1 deletion backend/app/Repository/Eloquent/AttendeeRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public function getAttendeesByCheckInShortId(string $shortId, QueryParamsDTO $pa
->whereIn('attendees.status',[AttendeeStatus::ACTIVE->name, AttendeeStatus::CANCELLED->name, AttendeeStatus::AWAITING_PAYMENT->name])
->whereIn('orders.status', [OrderStatus::COMPLETED->name, OrderStatus::AWAITING_OFFLINE_PAYMENT->name]);

$this->loadRelation(new Relationship(AttendeeCheckInDomainObject::class, name: 'check_in'));
$this->loadRelation(new Relationship(AttendeeCheckInDomainObject::class, name: 'check_ins'));

return $this->simplePaginateWhere(
where: $where,
Expand Down
4 changes: 2 additions & 2 deletions backend/app/Repository/Eloquent/CheckInListRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public function getCheckedInAttendeeCountById(int $checkInListId): CheckedInAtte
COUNT(DISTINCT vci.attendee_id) AS checked_in_attendees
FROM check_in_lists cil
LEFT JOIN valid_attendees va ON va.check_in_list_id = cil.id
LEFT JOIN valid_check_ins vci ON vci.attendee_id = va.id
LEFT JOIN valid_check_ins vci ON vci.attendee_id = va.id AND vci.check_in_list_id = va.check_in_list_id
WHERE cil.id = :check_in_list_id
AND cil.deleted_at IS NULL
GROUP BY cil.id;
Expand Down Expand Up @@ -106,7 +106,7 @@ public function getCheckedInAttendeeCountByIds(array $checkInListIds): Collectio
COUNT(DISTINCT vci.attendee_id) AS checked_in_attendees
FROM check_in_lists cil
LEFT JOIN valid_attendees va ON va.check_in_list_id = cil.id
LEFT JOIN valid_check_ins vci ON vci.attendee_id = va.id
LEFT JOIN valid_check_ins vci ON vci.attendee_id = va.id AND vci.check_in_list_id = va.check_in_list_id
WHERE cil.id IN ($placeholders)
AND cil.deleted_at IS NULL
GROUP BY cil.id;
Expand Down
6 changes: 3 additions & 3 deletions backend/app/Resources/Attendee/AttendeeResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ public function toArray(Request $request): array
!is_null($this->getProduct()),
fn() => new ProductResource($this->getProduct()),
),
'check_in' => $this->when(
condition: $this->getCheckIn() !== null,
value: fn() => new AttendeeCheckInResource($this->getCheckIn()),
'check_ins' => $this->when(
condition: $this->getCheckIns() !== null,
value: fn() => AttendeeCheckInResource::collection($this->getCheckIns()),
),
'order' => $this->when(
condition: !is_null($this->getOrder()),
Expand Down
4 changes: 4 additions & 0 deletions backend/app/Resources/CheckInList/AttendeeCheckInResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public function toArray($request): array
'event_id' => $this->getEventId(),
'short_id' => $this->getShortId(),
'created_at' => $this->getCreatedAt(),
'check_in_list' => $this->when(
!is_null($this->getCheckInList()),
fn() => (new CheckInListResource($this->getCheckInList()))->toArray($request)
),
'attendee' => $this->when(
!is_null($this->getAttendee()),
fn() => (new AttendeeResource($this->getAttendee()))->toArray($request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace HiEvents\Services\Application\Handlers\CheckInList\Public;

use HiEvents\DomainObjects\AttendeeDomainObject;
use HiEvents\DomainObjects\CheckInListDomainObject;
use HiEvents\DomainObjects\EventDomainObject;
use HiEvents\DomainObjects\Generated\CheckInListDomainObjectAbstract;
Expand Down Expand Up @@ -42,7 +43,15 @@ public function handle(string $shortId, QueryParamsDTO $queryParams): Paginator

$this->validateCheckInListIsActive($checkInList);

return $this->attendeeRepository->getAttendeesByCheckInShortId($shortId, $queryParams);
$attendees = $this->attendeeRepository->getAttendeesByCheckInShortId($shortId, $queryParams);

// Set the check-in for each attendee
$attendees->getCollection()->transform(function (AttendeeDomainObject $attendee) use ($checkInList) {
$attendee->setCheckIn($attendee->getCheckIns()?->first(fn ($checkIn) => $checkIn->getCheckInListId() === $checkInList->getId()));
return $attendee;
});

return $attendees;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public function checkInAttendees(

$attendees = $this->fetchAttendees($attendeesAndActions);
$eventSettings = $this->fetchEventSettings($checkInList->getEventId());
$existingCheckIns = $this->fetchExistingCheckIns($attendees, $checkInList->getEventId());
$existingCheckIns = $this->fetchExistingCheckIns($attendees, $checkInList);

return $this->processAttendeeCheckIns(
$attendees,
Expand Down Expand Up @@ -103,19 +103,20 @@ private function fetchEventSettings(int $eventId): EventSettingDomainObject

/**
* @param Collection<int, AttendeeDomainObject> $attendees
* @param int $eventId
* @param CheckInListDomainObject $checkInList
* @return Collection
* @throws Exception
*/
private function fetchExistingCheckIns(Collection $attendees, int $eventId): Collection
private function fetchExistingCheckIns(Collection $attendees, CheckInListDomainObject $checkInList): Collection
{
$attendeeIds = $attendees->map(fn(AttendeeDomainObject $attendee) => $attendee->getId())->toArray();

return $this->attendeeCheckInRepository->findWhereIn(
field: AttendeeCheckInDomainObjectAbstract::ATTENDEE_ID,
values: $attendeeIds,
additionalWhere: [
AttendeeCheckInDomainObjectAbstract::EVENT_ID => $eventId,
AttendeeCheckInDomainObjectAbstract::EVENT_ID => $checkInList->getEventId(),
AttendeeCheckInDomainObjectAbstract::CHECK_IN_LIST_ID => $checkInList->getId(),
],
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public function deleteAttendeeCheckIn(
->loadRelation(new Relationship(AttendeeDomainObject::class, name: 'attendee'))
->findFirstWhere([
AttendeeCheckInDomainObjectAbstract::SHORT_ID => $checkInShortId,
AttendeeCheckInDomainObjectAbstract::CHECK_IN_LIST_ID => $this
->checkInListDataService
->getCheckInList($checkInListShortId)->getId(),
]);

if ($checkIn === null) {
Expand Down
23 changes: 15 additions & 8 deletions frontend/src/components/common/AttendeeDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import classes from "./AttendeeDetails.module.scss";
import {t} from "@lingui/macro";
import {getAttendeeProductTitle} from "../../../utilites/products.ts";
import {getLocaleName, SupportedLocales} from "../../../locales.ts";
import {relativeDate} from "../../../utilites/dates.ts";

export const AttendeeDetails = ({attendee}: { attendee: Attendee }) => {
return (
Expand All @@ -24,14 +25,6 @@ export const AttendeeDetails = ({attendee}: { attendee: Attendee }) => {
<Anchor href={'mailto:' + attendee.email} target={'_blank'}>{attendee.email}</Anchor>
</div>
</div>
<div className={classes.block}>
<div className={classes.title}>
{t`Checked In`}
</div>
<div className={classes.amount}>
{attendee.check_in ? t`Yes` : t`No`}
</div>
</div>
<div className={classes.block}>
<div className={classes.title}>
{t`Product`}
Expand All @@ -48,6 +41,20 @@ export const AttendeeDetails = ({attendee}: { attendee: Attendee }) => {
{getLocaleName(attendee.locale as SupportedLocales)}
</div>
</div>
{attendee.check_ins && attendee.check_ins.length > 0 && (
<div className={classes.block}>
<div className={classes.title}>
{t`Check-Ins`}
</div>
<div className={classes.value}>
{attendee.check_ins.map((checkIn) => (
<div key={checkIn.id}>
<strong>{checkIn.check_in_list?.name}</strong> - {relativeDate(checkIn.created_at)}
</div>
))}
</div>
</div>
)}
</div>
);
}
4 changes: 1 addition & 3 deletions frontend/src/components/common/CheckInListList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ export const CheckInListList = ({checkInLists, openCreateModal}: CheckInListList
<p>
<Trans>
<p>
Check-in lists help manage attendee entry for your event. You can associate multiple
tickets with a check-in list and ensure only those with valid tickets can enter.
</p>
Check-in lists help you manage event entry by day, area, or ticket type. You can link tickets to specific lists such as VIP zones or Day 1 passes and share a secure check-in link with staff. No account is required. Check-in works on mobile, desktop, or tablet, using a device camera or HID USB scanner. </p>
</Trans>
</p>
<Button
Expand Down
Loading
Loading