Skip to content

Commit 8de344e

Browse files
authored
Merge pull request #320 from HiEventsDev/feature/attendee-notes
Add ability to add notes to an attendee + Refactor Attendee modal
2 parents 92fd1f3 + 82b5491 commit 8de344e

File tree

26 files changed

+382
-293
lines changed

26 files changed

+382
-293
lines changed

backend/app/DomainObjects/Generated/AttendeeDomainObjectAbstract.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ abstract class AttendeeDomainObjectAbstract extends \HiEvents\DomainObjects\Abst
2828
final public const UPDATED_AT = 'updated_at';
2929
final public const DELETED_AT = 'deleted_at';
3030
final public const LOCALE = 'locale';
31+
final public const NOTES = 'notes';
3132

3233
protected int $id;
3334
protected int $order_id;
@@ -47,6 +48,7 @@ abstract class AttendeeDomainObjectAbstract extends \HiEvents\DomainObjects\Abst
4748
protected string $updated_at;
4849
protected ?string $deleted_at = null;
4950
protected string $locale = 'en';
51+
protected ?string $notes = null;
5052

5153
public function toArray(): array
5254
{
@@ -69,6 +71,7 @@ public function toArray(): array
6971
'updated_at' => $this->updated_at ?? null,
7072
'deleted_at' => $this->deleted_at ?? null,
7173
'locale' => $this->locale ?? null,
74+
'notes' => $this->notes ?? null,
7275
];
7376
}
7477

@@ -269,4 +272,15 @@ public function getLocale(): string
269272
{
270273
return $this->locale;
271274
}
275+
276+
public function setNotes(?string $notes): self
277+
{
278+
$this->notes = $notes;
279+
return $this;
280+
}
281+
282+
public function getNotes(): ?string
283+
{
284+
return $this->notes;
285+
}
272286
}

backend/app/Http/Actions/Attendees/EditAttendeeAction.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public function __invoke(EditAttendeeRequest $request, int $eventId, int $attend
3737
'product_price_id' => $request->input('product_price_id'),
3838
'event_id' => $eventId,
3939
'attendee_id' => $attendeeId,
40+
'notes' => $request->input('notes'),
4041
]));
4142
} catch (NoTicketsAvailableException $exception) {
4243
throw ValidationException::withMessages([

backend/app/Http/Actions/Orders/GetOrderAction.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace HiEvents\Http\Actions\Orders;
44

55
use HiEvents\DomainObjects\AttendeeDomainObject;
6+
use HiEvents\DomainObjects\EventDomainObject;
67
use HiEvents\DomainObjects\OrderItemDomainObject;
78
use HiEvents\DomainObjects\QuestionAndAnswerViewDomainObject;
89
use HiEvents\Http\Actions\BaseAction;
@@ -21,6 +22,8 @@ public function __construct(OrderRepositoryInterface $orderRepository)
2122

2223
public function __invoke(int $eventId, int $orderId): JsonResponse
2324
{
25+
$this->isActionAuthorized($eventId, EventDomainObject::class);
26+
2427
$order = $this->orderRepository
2528
->loadRelation(OrderItemDomainObject::class)
2629
->loadRelation(AttendeeDomainObject::class)

backend/app/Http/Request/Attendee/EditAttendeeRequest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public function rules(): array
1515
'last_name' => RulesHelper::REQUIRED_STRING,
1616
'product_id' => RulesHelper::REQUIRED_NUMERIC,
1717
'product_price_id' => RulesHelper::REQUIRED_NUMERIC,
18+
'notes' => RulesHelper::OPTIONAL_TEXT_MEDIUM_LENGTH,
1819
];
1920
}
2021

@@ -29,6 +30,7 @@ public function messages(): array
2930
'product_price_id.required' => __('Product price is required'),
3031
'product_id.numeric' => '',
3132
'product_price_id.numeric' => '',
33+
'notes.max' => __('Notes must be less than 2000 characters'),
3234
];
3335
}
3436
}

backend/app/Http/Request/ProductCategory/UpsertProductCategoryRequest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public function rules(): array
1212
'name' => ['string', 'required', 'max:50'],
1313
'description' => ['string', 'max:255', 'nullable'],
1414
'is_hidden' => ['boolean', 'required'],
15-
'no_products_message' => ['string', 'max:255', 'required'],
15+
'no_products_message' => ['string', 'max:255', 'nullable'],
1616
];
1717
}
1818
}

backend/app/Resources/Attendee/AttendeeResource.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public function toArray(Request $request): array
3030
'public_id' => $this->getPublicId(),
3131
'short_id' => $this->getShortId(),
3232
'locale' => $this->getLocale(),
33+
'notes' => $this->getNotes(),
3334
'product' => $this->when(
3435
!is_null($this->getProduct()),
3536
fn() => new ProductResource($this->getProduct()),

backend/app/Services/Application/Handlers/Attendee/DTO/EditAttendeeDTO.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
class EditAttendeeDTO extends BaseDTO
88
{
99
public function __construct(
10-
public string $first_name,
11-
public string $last_name,
12-
public string $email,
13-
public int $product_id,
14-
public int $product_price_id,
15-
public int $event_id,
16-
public int $attendee_id,
10+
public string $first_name,
11+
public string $last_name,
12+
public string $email,
13+
public int $product_id,
14+
public int $product_price_id,
15+
public int $event_id,
16+
public int $attendee_id,
17+
public ?string $notes = null,
1718
)
1819
{
1920
}

backend/app/Services/Application/Handlers/Attendee/EditAttendeeHandler.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use HiEvents\DomainObjects\Enums\ProductPriceType;
77
use HiEvents\DomainObjects\Generated\AttendeeDomainObjectAbstract;
88
use HiEvents\DomainObjects\Generated\ProductDomainObjectAbstract;
9+
use HiEvents\DomainObjects\ProductDomainObject;
910
use HiEvents\DomainObjects\ProductPriceDomainObject;
1011
use HiEvents\Exceptions\NoTicketsAvailableException;
1112
use HiEvents\Repository\Interfaces\AttendeeRepositoryInterface;
@@ -59,6 +60,7 @@ private function updateAttendee(EditAttendeeDTO $editAttendeeDTO): AttendeeDomai
5960
'last_name' => $editAttendeeDTO->last_name,
6061
'email' => $editAttendeeDTO->email,
6162
'product_id' => $editAttendeeDTO->product_id,
63+
'notes' => $editAttendeeDTO->notes,
6264
], [
6365
'event_id' => $editAttendeeDTO->event_id,
6466
]);
@@ -70,6 +72,7 @@ private function updateAttendee(EditAttendeeDTO $editAttendeeDTO): AttendeeDomai
7072
*/
7173
private function validateProductId(EditAttendeeDTO $editAttendeeDTO): void
7274
{
75+
/** @var ProductDomainObject $product */
7376
$product = $this->productRepository
7477
->loadRelation(ProductPriceDomainObject::class)
7578
->findFirstWhere([

backend/app/Services/Application/Handlers/ProductCategory/CreateProductCategoryHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public function handle(UpsertProductCategoryDTO $dto): ProductCategoryDomainObje
2121
isHidden: $dto->is_hidden,
2222
eventId: $dto->event_id,
2323
description: $dto->description,
24-
noProductsMessage: $dto->no_products_message,
24+
noProductsMessage: $dto->no_products_message ?? __('There are no products available in this category'),
2525
);
2626
}
2727
}

backend/app/Services/Application/Handlers/ProductCategory/EditProductCategoryHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public function handle(UpsertProductCategoryDTO $dto): ProductCategoryDomainObje
2121
'name' => $dto->name,
2222
'is_hidden' => $dto->is_hidden,
2323
'description' => $dto->description,
24-
'no_products_message' => $dto->no_products_message,
24+
'no_products_message' => $dto->no_products_message ?? __('There are no products available in this category'),
2525
],
2626
where: [
2727
'id' => $dto->product_category_id,

backend/app/Services/Domain/Event/DuplicateEventService.php

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use HiEvents\DomainObjects\CapacityAssignmentDomainObject;
66
use HiEvents\DomainObjects\CheckInListDomainObject;
77
use HiEvents\DomainObjects\Enums\EventImageType;
8+
use HiEvents\DomainObjects\Enums\QuestionBelongsTo;
89
use HiEvents\DomainObjects\EventDomainObject;
910
use HiEvents\DomainObjects\EventSettingDomainObject;
1011
use HiEvents\DomainObjects\ImageDomainObject;
@@ -79,8 +80,12 @@ public function duplicateEvent(
7980
cloneEventSettings: $duplicateSettings,
8081
);
8182

83+
if ($duplicateQuestions) {
84+
$this->clonePerOrderQuestions($event, $newEvent->getId());
85+
}
86+
8287
if ($duplicateProducts) {
83-
$this->cloneExistingProducts(
88+
$this->cloneExistingTickets(
8489
event: $event,
8590
newEventId: $newEvent->getId(),
8691
duplicateQuestions: $duplicateQuestions,
@@ -131,7 +136,7 @@ private function cloneExistingEvent(EventDomainObject $event, bool $cloneEventSe
131136
/**
132137
* @throws Throwable
133138
*/
134-
private function cloneExistingProducts(
139+
private function cloneExistingTickets(
135140
EventDomainObject $event,
136141
int $newEventId,
137142
bool $duplicateQuestions,
@@ -140,20 +145,20 @@ private function cloneExistingProducts(
140145
bool $duplicateCheckInLists,
141146
): array
142147
{
143-
$oldProductToNewProductMap = [];
148+
$oldTicketToNewTicketMap = [];
144149

145-
foreach ($event->getProducts() as $product) {
146-
$product->setEventId($newEventId);
147-
$newProduct = $this->createProductService->createProduct(
148-
product: $product,
150+
foreach ($event->getProducts() as $ticket) {
151+
$ticket->setEventId($newEventId);
152+
$newTicket = $this->createProductService->createTicket(
153+
ticket: $ticket,
149154
accountId: $event->getAccountId(),
150-
taxAndFeeIds: $product->getTaxAndFees()?->map(fn($taxAndFee) => $taxAndFee->getId())?->toArray(),
155+
taxAndFeeIds: $ticket->getTaxAndFees()?->map(fn($taxAndFee) => $taxAndFee->getId())?->toArray(),
151156
);
152-
$oldProductToNewProductMap[$product->getId()] = $newProduct->getId();
157+
$oldTicketToNewTicketMap[$ticket->getId()] = $newTicket->getId();
153158
}
154159

155160
if ($duplicateQuestions) {
156-
$this->cloneQuestions($event, $newEventId, $oldProductToNewProductMap);
161+
$this->clonePerTicketQuestions($event, $newEventId, $oldTicketToNewTicketMap);
157162
}
158163

159164
if ($duplicatePromoCodes) {
@@ -174,23 +179,47 @@ private function cloneExistingProducts(
174179
/**
175180
* @throws Throwable
176181
*/
177-
private function cloneQuestions(EventDomainObject $event, int $newEventId, array $oldProductToNewProductMap): void
182+
private function clonePerTicketQuestions(EventDomainObject $event, int $newEventId, array $oldTicketToNewTicketMap): void
178183
{
179184
foreach ($event->getQuestions() as $question) {
180-
$this->createQuestionService->createQuestion(
181-
(new QuestionDomainObject())
182-
->setTitle($question->getTitle())
183-
->setEventId($newEventId)
184-
->setBelongsTo($question->getBelongsTo())
185-
->setType($question->getType())
186-
->setRequired($question->getRequired())
187-
->setOptions($question->getOptions())
188-
->setIsHidden($question->getIsHidden()),
189-
array_map(
190-
static fn(ProductDomainObject $product) => $oldProductToNewProductMap[$product->getId()],
191-
$question->getProducts()?->all(),
192-
),
193-
);
185+
if ($question->getBelongsTo() === QuestionBelongsTo::TICKET->name) {
186+
$this->createQuestionService->createQuestion(
187+
(new QuestionDomainObject())
188+
->setTitle($question->getTitle())
189+
->setEventId($newEventId)
190+
->setBelongsTo($question->getBelongsTo())
191+
->setType($question->getType())
192+
->setRequired($question->getRequired())
193+
->setOptions($question->getOptions())
194+
->setIsHidden($question->getIsHidden()),
195+
array_map(
196+
static fn(ProductDomainObject $ticket) => $oldTicketToNewTicketMap[$ticket->getId()],
197+
$question->getTickets()?->all(),
198+
),
199+
);
200+
}
201+
}
202+
}
203+
204+
/**
205+
* @throws Throwable
206+
*/
207+
private function clonePerOrderQuestions(EventDomainObject $event, int $newEventId): void
208+
{
209+
foreach ($event->getQuestions() as $question) {
210+
if ($question->getBelongsTo() === QuestionBelongsTo::ORDER->name) {
211+
$this->createQuestionService->createQuestion(
212+
(new QuestionDomainObject())
213+
->setTitle($question->getTitle())
214+
->setEventId($newEventId)
215+
->setBelongsTo($question->getBelongsTo())
216+
->setType($question->getType())
217+
->setRequired($question->getRequired())
218+
->setOptions($question->getOptions())
219+
->setIsHidden($question->getIsHidden()),
220+
[],
221+
);
222+
}
194223
}
195224
}
196225

backend/app/Validators/Rules/RulesHelper.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ class RulesHelper
1616

1717
public const REQUIRED_EMAIL = ['email' , 'required', 'max:100'];
1818

19+
public const OPTIONAL_TEXT_MEDIUM_LENGTH = ['string', 'max:2000', 'nullable'];
1920
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration {
8+
public function up(): void
9+
{
10+
Schema::table('attendees', static function (Blueprint $table) {
11+
$table->text('notes')->nullable();
12+
});
13+
}
14+
15+
public function down(): void
16+
{
17+
Schema::table('attendees', static function (Blueprint $table) {
18+
$table->dropColumn('notes');
19+
});
20+
}
21+
};

docker/all-in-one/.env

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
# See the README.md for informaiton on how to generate the JWT_SECRET and APP_KEY
1+
# See the README.md file for informaiton on how to generate the JWT_SECRET and APP_KEY
22
APP_KEY=
33
JWT_SECRET=
44

5+
# Frontend variables (Always prefixed with VITE_)
56
VITE_FRONTEND_URL=http://localhost:8123
67
VITE_API_URL_CLIENT=http://localhost:8123/api
78
VITE_API_URL_SERVER=http://localhost:80/api
89
VITE_STRIPE_PUBLISHABLE_KEY=pk_test
910

11+
# Backend variables
12+
# These values may not be suitable for production environments.
13+
# Please refer to the documentation for more information on how to configure these values
14+
# https://hi.events/docs/getting-started/deploying
1015
LOG_CHANNEL=stderr
1116
QUEUE_CONNECTION=sync
1217
MAIL_MAILER=log
@@ -15,5 +20,6 @@ FILESYSTEM_PUBLIC_DISK=public
1520
FILESYSTEM_PRIVATE_DISK=local
1621

1722
APP_CDN_URL=http://localhost:8123/storage
23+
APP_FRONTEND_URL=http://localhost:8123
1824

1925
DATABASE_URL=postgresql://postgres:secret@postgres:5432/hi-events

docker/all-in-one/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ services:
1515
- QUEUE_CONNECTION=${QUEUE_CONNECTION}
1616
- MAIL_MAILER=${MAIL_MAILER}
1717
- APP_KEY=${APP_KEY}
18+
- APP_FRONTEND_URL=${APP_FRONTEND_URL}
1819
- JWT_SECRET=${JWT_SECRET}
1920
- FILESYSTEM_PUBLIC_DISK=${FILESYSTEM_PUBLIC_DISK}
2021
- FILESYSTEM_PRIVATE_DISK=${FILESYSTEM_PRIVATE_DISK}

frontend/src/api/attendee.client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface EditAttendeeRequest {
88
first_name: string;
99
last_name: string;
1010
email: string;
11+
notes?: string;
1112
product_id?: IdParam;
1213
product_price_id?: IdParam;
1314
status?: string;

frontend/src/components/common/AttendeeDetails/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {getLocaleName, SupportedLocales} from "../../../locales.ts";
77

88
export const AttendeeDetails = ({attendee}: { attendee: Attendee }) => {
99
return (
10-
<div className={classes.orderDetails} variant={'lightGray'}>
10+
<div className={classes.orderDetails}>
1111
<div className={classes.block}>
1212
<div className={classes.title}>
1313
{t`Name`}
@@ -29,7 +29,7 @@ export const AttendeeDetails = ({attendee}: { attendee: Attendee }) => {
2929
{t`Status`}
3030
</div>
3131
<div className={classes.amount}>
32-
{attendee.status}
32+
{attendee.status === 'ACTIVE' ? <span style={{color: '#0d7553'}}>{t`Active`}</span> : <span style={{color: '#EF4444'}}>{t`Canceled`}</span>}
3333
</div>
3434
</div>
3535
<div className={classes.block}>

0 commit comments

Comments
 (0)