diff --git a/backend/app/DomainObjects/Generated/AttendeeDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/AttendeeDomainObjectAbstract.php index 6097de8955..be3ca97e0e 100644 --- a/backend/app/DomainObjects/Generated/AttendeeDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/AttendeeDomainObjectAbstract.php @@ -28,6 +28,7 @@ abstract class AttendeeDomainObjectAbstract extends \HiEvents\DomainObjects\Abst final public const UPDATED_AT = 'updated_at'; final public const DELETED_AT = 'deleted_at'; final public const LOCALE = 'locale'; + final public const NOTES = 'notes'; protected int $id; protected int $order_id; @@ -47,6 +48,7 @@ abstract class AttendeeDomainObjectAbstract extends \HiEvents\DomainObjects\Abst protected string $updated_at; protected ?string $deleted_at = null; protected string $locale = 'en'; + protected ?string $notes = null; public function toArray(): array { @@ -69,6 +71,7 @@ public function toArray(): array 'updated_at' => $this->updated_at ?? null, 'deleted_at' => $this->deleted_at ?? null, 'locale' => $this->locale ?? null, + 'notes' => $this->notes ?? null, ]; } @@ -269,4 +272,15 @@ public function getLocale(): string { return $this->locale; } + + public function setNotes(?string $notes): self + { + $this->notes = $notes; + return $this; + } + + public function getNotes(): ?string + { + return $this->notes; + } } diff --git a/backend/app/Http/Actions/Attendees/EditAttendeeAction.php b/backend/app/Http/Actions/Attendees/EditAttendeeAction.php index 504be54eef..ede204d4fc 100644 --- a/backend/app/Http/Actions/Attendees/EditAttendeeAction.php +++ b/backend/app/Http/Actions/Attendees/EditAttendeeAction.php @@ -37,6 +37,7 @@ public function __invoke(EditAttendeeRequest $request, int $eventId, int $attend 'product_price_id' => $request->input('product_price_id'), 'event_id' => $eventId, 'attendee_id' => $attendeeId, + 'notes' => $request->input('notes'), ])); } catch (NoTicketsAvailableException $exception) { throw ValidationException::withMessages([ diff --git a/backend/app/Http/Actions/Orders/GetOrderAction.php b/backend/app/Http/Actions/Orders/GetOrderAction.php index 9773221e56..b72c5581e8 100644 --- a/backend/app/Http/Actions/Orders/GetOrderAction.php +++ b/backend/app/Http/Actions/Orders/GetOrderAction.php @@ -3,6 +3,7 @@ namespace HiEvents\Http\Actions\Orders; use HiEvents\DomainObjects\AttendeeDomainObject; +use HiEvents\DomainObjects\EventDomainObject; use HiEvents\DomainObjects\OrderItemDomainObject; use HiEvents\DomainObjects\QuestionAndAnswerViewDomainObject; use HiEvents\Http\Actions\BaseAction; @@ -21,6 +22,8 @@ public function __construct(OrderRepositoryInterface $orderRepository) public function __invoke(int $eventId, int $orderId): JsonResponse { + $this->isActionAuthorized($eventId, EventDomainObject::class); + $order = $this->orderRepository ->loadRelation(OrderItemDomainObject::class) ->loadRelation(AttendeeDomainObject::class) diff --git a/backend/app/Http/Request/Attendee/EditAttendeeRequest.php b/backend/app/Http/Request/Attendee/EditAttendeeRequest.php index 0badf3043d..780c42a80b 100644 --- a/backend/app/Http/Request/Attendee/EditAttendeeRequest.php +++ b/backend/app/Http/Request/Attendee/EditAttendeeRequest.php @@ -15,6 +15,7 @@ public function rules(): array 'last_name' => RulesHelper::REQUIRED_STRING, 'product_id' => RulesHelper::REQUIRED_NUMERIC, 'product_price_id' => RulesHelper::REQUIRED_NUMERIC, + 'notes' => RulesHelper::OPTIONAL_TEXT_MEDIUM_LENGTH, ]; } @@ -29,6 +30,7 @@ public function messages(): array 'product_price_id.required' => __('Product price is required'), 'product_id.numeric' => '', 'product_price_id.numeric' => '', + 'notes.max' => __('Notes must be less than 2000 characters'), ]; } } diff --git a/backend/app/Http/Request/ProductCategory/UpsertProductCategoryRequest.php b/backend/app/Http/Request/ProductCategory/UpsertProductCategoryRequest.php index 11e19e91a5..87a7ccdb97 100644 --- a/backend/app/Http/Request/ProductCategory/UpsertProductCategoryRequest.php +++ b/backend/app/Http/Request/ProductCategory/UpsertProductCategoryRequest.php @@ -12,7 +12,7 @@ public function rules(): array 'name' => ['string', 'required', 'max:50'], 'description' => ['string', 'max:255', 'nullable'], 'is_hidden' => ['boolean', 'required'], - 'no_products_message' => ['string', 'max:255', 'required'], + 'no_products_message' => ['string', 'max:255', 'nullable'], ]; } } diff --git a/backend/app/Resources/Attendee/AttendeeResource.php b/backend/app/Resources/Attendee/AttendeeResource.php index a4b79c84a5..bc7b324f53 100644 --- a/backend/app/Resources/Attendee/AttendeeResource.php +++ b/backend/app/Resources/Attendee/AttendeeResource.php @@ -30,6 +30,7 @@ public function toArray(Request $request): array 'public_id' => $this->getPublicId(), 'short_id' => $this->getShortId(), 'locale' => $this->getLocale(), + 'notes' => $this->getNotes(), 'product' => $this->when( !is_null($this->getProduct()), fn() => new ProductResource($this->getProduct()), diff --git a/backend/app/Services/Application/Handlers/Attendee/DTO/EditAttendeeDTO.php b/backend/app/Services/Application/Handlers/Attendee/DTO/EditAttendeeDTO.php index d683a42877..155c602bc3 100644 --- a/backend/app/Services/Application/Handlers/Attendee/DTO/EditAttendeeDTO.php +++ b/backend/app/Services/Application/Handlers/Attendee/DTO/EditAttendeeDTO.php @@ -7,13 +7,14 @@ class EditAttendeeDTO extends BaseDTO { public function __construct( - public string $first_name, - public string $last_name, - public string $email, - public int $product_id, - public int $product_price_id, - public int $event_id, - public int $attendee_id, + public string $first_name, + public string $last_name, + public string $email, + public int $product_id, + public int $product_price_id, + public int $event_id, + public int $attendee_id, + public ?string $notes = null, ) { } diff --git a/backend/app/Services/Application/Handlers/Attendee/EditAttendeeHandler.php b/backend/app/Services/Application/Handlers/Attendee/EditAttendeeHandler.php index c6f7c874f5..567e1d4d94 100644 --- a/backend/app/Services/Application/Handlers/Attendee/EditAttendeeHandler.php +++ b/backend/app/Services/Application/Handlers/Attendee/EditAttendeeHandler.php @@ -6,6 +6,7 @@ use HiEvents\DomainObjects\Enums\ProductPriceType; use HiEvents\DomainObjects\Generated\AttendeeDomainObjectAbstract; use HiEvents\DomainObjects\Generated\ProductDomainObjectAbstract; +use HiEvents\DomainObjects\ProductDomainObject; use HiEvents\DomainObjects\ProductPriceDomainObject; use HiEvents\Exceptions\NoTicketsAvailableException; use HiEvents\Repository\Interfaces\AttendeeRepositoryInterface; @@ -59,6 +60,7 @@ private function updateAttendee(EditAttendeeDTO $editAttendeeDTO): AttendeeDomai 'last_name' => $editAttendeeDTO->last_name, 'email' => $editAttendeeDTO->email, 'product_id' => $editAttendeeDTO->product_id, + 'notes' => $editAttendeeDTO->notes, ], [ 'event_id' => $editAttendeeDTO->event_id, ]); @@ -70,6 +72,7 @@ private function updateAttendee(EditAttendeeDTO $editAttendeeDTO): AttendeeDomai */ private function validateProductId(EditAttendeeDTO $editAttendeeDTO): void { + /** @var ProductDomainObject $product */ $product = $this->productRepository ->loadRelation(ProductPriceDomainObject::class) ->findFirstWhere([ diff --git a/backend/app/Services/Application/Handlers/ProductCategory/CreateProductCategoryHandler.php b/backend/app/Services/Application/Handlers/ProductCategory/CreateProductCategoryHandler.php index 17d50cc33b..73784755b2 100644 --- a/backend/app/Services/Application/Handlers/ProductCategory/CreateProductCategoryHandler.php +++ b/backend/app/Services/Application/Handlers/ProductCategory/CreateProductCategoryHandler.php @@ -21,7 +21,7 @@ public function handle(UpsertProductCategoryDTO $dto): ProductCategoryDomainObje isHidden: $dto->is_hidden, eventId: $dto->event_id, description: $dto->description, - noProductsMessage: $dto->no_products_message, + noProductsMessage: $dto->no_products_message ?? __('There are no products available in this category'), ); } } diff --git a/backend/app/Services/Application/Handlers/ProductCategory/EditProductCategoryHandler.php b/backend/app/Services/Application/Handlers/ProductCategory/EditProductCategoryHandler.php index 83f7f3683c..105adde141 100644 --- a/backend/app/Services/Application/Handlers/ProductCategory/EditProductCategoryHandler.php +++ b/backend/app/Services/Application/Handlers/ProductCategory/EditProductCategoryHandler.php @@ -21,7 +21,7 @@ public function handle(UpsertProductCategoryDTO $dto): ProductCategoryDomainObje 'name' => $dto->name, 'is_hidden' => $dto->is_hidden, 'description' => $dto->description, - 'no_products_message' => $dto->no_products_message, + 'no_products_message' => $dto->no_products_message ?? __('There are no products available in this category'), ], where: [ 'id' => $dto->product_category_id, diff --git a/backend/app/Services/Domain/Event/DuplicateEventService.php b/backend/app/Services/Domain/Event/DuplicateEventService.php index 96dffb1782..cb58643f0b 100644 --- a/backend/app/Services/Domain/Event/DuplicateEventService.php +++ b/backend/app/Services/Domain/Event/DuplicateEventService.php @@ -5,6 +5,7 @@ use HiEvents\DomainObjects\CapacityAssignmentDomainObject; use HiEvents\DomainObjects\CheckInListDomainObject; use HiEvents\DomainObjects\Enums\EventImageType; +use HiEvents\DomainObjects\Enums\QuestionBelongsTo; use HiEvents\DomainObjects\EventDomainObject; use HiEvents\DomainObjects\EventSettingDomainObject; use HiEvents\DomainObjects\ImageDomainObject; @@ -79,8 +80,12 @@ public function duplicateEvent( cloneEventSettings: $duplicateSettings, ); + if ($duplicateQuestions) { + $this->clonePerOrderQuestions($event, $newEvent->getId()); + } + if ($duplicateProducts) { - $this->cloneExistingProducts( + $this->cloneExistingTickets( event: $event, newEventId: $newEvent->getId(), duplicateQuestions: $duplicateQuestions, @@ -131,7 +136,7 @@ private function cloneExistingEvent(EventDomainObject $event, bool $cloneEventSe /** * @throws Throwable */ - private function cloneExistingProducts( + private function cloneExistingTickets( EventDomainObject $event, int $newEventId, bool $duplicateQuestions, @@ -140,20 +145,20 @@ private function cloneExistingProducts( bool $duplicateCheckInLists, ): array { - $oldProductToNewProductMap = []; + $oldTicketToNewTicketMap = []; - foreach ($event->getProducts() as $product) { - $product->setEventId($newEventId); - $newProduct = $this->createProductService->createProduct( - product: $product, + foreach ($event->getProducts() as $ticket) { + $ticket->setEventId($newEventId); + $newTicket = $this->createProductService->createTicket( + ticket: $ticket, accountId: $event->getAccountId(), - taxAndFeeIds: $product->getTaxAndFees()?->map(fn($taxAndFee) => $taxAndFee->getId())?->toArray(), + taxAndFeeIds: $ticket->getTaxAndFees()?->map(fn($taxAndFee) => $taxAndFee->getId())?->toArray(), ); - $oldProductToNewProductMap[$product->getId()] = $newProduct->getId(); + $oldTicketToNewTicketMap[$ticket->getId()] = $newTicket->getId(); } if ($duplicateQuestions) { - $this->cloneQuestions($event, $newEventId, $oldProductToNewProductMap); + $this->clonePerTicketQuestions($event, $newEventId, $oldTicketToNewTicketMap); } if ($duplicatePromoCodes) { @@ -174,23 +179,47 @@ private function cloneExistingProducts( /** * @throws Throwable */ - private function cloneQuestions(EventDomainObject $event, int $newEventId, array $oldProductToNewProductMap): void + private function clonePerTicketQuestions(EventDomainObject $event, int $newEventId, array $oldTicketToNewTicketMap): void { foreach ($event->getQuestions() as $question) { - $this->createQuestionService->createQuestion( - (new QuestionDomainObject()) - ->setTitle($question->getTitle()) - ->setEventId($newEventId) - ->setBelongsTo($question->getBelongsTo()) - ->setType($question->getType()) - ->setRequired($question->getRequired()) - ->setOptions($question->getOptions()) - ->setIsHidden($question->getIsHidden()), - array_map( - static fn(ProductDomainObject $product) => $oldProductToNewProductMap[$product->getId()], - $question->getProducts()?->all(), - ), - ); + if ($question->getBelongsTo() === QuestionBelongsTo::TICKET->name) { + $this->createQuestionService->createQuestion( + (new QuestionDomainObject()) + ->setTitle($question->getTitle()) + ->setEventId($newEventId) + ->setBelongsTo($question->getBelongsTo()) + ->setType($question->getType()) + ->setRequired($question->getRequired()) + ->setOptions($question->getOptions()) + ->setIsHidden($question->getIsHidden()), + array_map( + static fn(ProductDomainObject $ticket) => $oldTicketToNewTicketMap[$ticket->getId()], + $question->getTickets()?->all(), + ), + ); + } + } + } + + /** + * @throws Throwable + */ + private function clonePerOrderQuestions(EventDomainObject $event, int $newEventId): void + { + foreach ($event->getQuestions() as $question) { + if ($question->getBelongsTo() === QuestionBelongsTo::ORDER->name) { + $this->createQuestionService->createQuestion( + (new QuestionDomainObject()) + ->setTitle($question->getTitle()) + ->setEventId($newEventId) + ->setBelongsTo($question->getBelongsTo()) + ->setType($question->getType()) + ->setRequired($question->getRequired()) + ->setOptions($question->getOptions()) + ->setIsHidden($question->getIsHidden()), + [], + ); + } } } diff --git a/backend/app/Validators/Rules/RulesHelper.php b/backend/app/Validators/Rules/RulesHelper.php index 20826cfe69..a68729c2de 100644 --- a/backend/app/Validators/Rules/RulesHelper.php +++ b/backend/app/Validators/Rules/RulesHelper.php @@ -16,4 +16,5 @@ class RulesHelper public const REQUIRED_EMAIL = ['email' , 'required', 'max:100']; + public const OPTIONAL_TEXT_MEDIUM_LENGTH = ['string', 'max:2000', 'nullable']; } diff --git a/backend/database/migrations/2024_12_09_234323_add_notes_to_attendees_table.php b/backend/database/migrations/2024_12_09_234323_add_notes_to_attendees_table.php new file mode 100644 index 0000000000..6ad2a1dea1 --- /dev/null +++ b/backend/database/migrations/2024_12_09_234323_add_notes_to_attendees_table.php @@ -0,0 +1,21 @@ +text('notes')->nullable(); + }); + } + + public function down(): void + { + Schema::table('attendees', static function (Blueprint $table) { + $table->dropColumn('notes'); + }); + } +}; diff --git a/docker/all-in-one/.env b/docker/all-in-one/.env index 72fd12f582..a76047ce19 100644 --- a/docker/all-in-one/.env +++ b/docker/all-in-one/.env @@ -1,12 +1,17 @@ -# See the README.md for informaiton on how to generate the JWT_SECRET and APP_KEY +# See the README.md file for informaiton on how to generate the JWT_SECRET and APP_KEY APP_KEY= JWT_SECRET= +# Frontend variables (Always prefixed with VITE_) VITE_FRONTEND_URL=http://localhost:8123 VITE_API_URL_CLIENT=http://localhost:8123/api VITE_API_URL_SERVER=http://localhost:80/api VITE_STRIPE_PUBLISHABLE_KEY=pk_test +# Backend variables +# These values may not be suitable for production environments. +# Please refer to the documentation for more information on how to configure these values +# https://hi.events/docs/getting-started/deploying LOG_CHANNEL=stderr QUEUE_CONNECTION=sync MAIL_MAILER=log @@ -15,5 +20,6 @@ FILESYSTEM_PUBLIC_DISK=public FILESYSTEM_PRIVATE_DISK=local APP_CDN_URL=http://localhost:8123/storage +APP_FRONTEND_URL=http://localhost:8123 DATABASE_URL=postgresql://postgres:secret@postgres:5432/hi-events diff --git a/docker/all-in-one/docker-compose.yml b/docker/all-in-one/docker-compose.yml index 68c5a79c45..90f4528561 100644 --- a/docker/all-in-one/docker-compose.yml +++ b/docker/all-in-one/docker-compose.yml @@ -15,6 +15,7 @@ services: - QUEUE_CONNECTION=${QUEUE_CONNECTION} - MAIL_MAILER=${MAIL_MAILER} - APP_KEY=${APP_KEY} + - APP_FRONTEND_URL=${APP_FRONTEND_URL} - JWT_SECRET=${JWT_SECRET} - FILESYSTEM_PUBLIC_DISK=${FILESYSTEM_PUBLIC_DISK} - FILESYSTEM_PRIVATE_DISK=${FILESYSTEM_PRIVATE_DISK} diff --git a/frontend/src/api/attendee.client.ts b/frontend/src/api/attendee.client.ts index 6f09fcbe9e..ba44fb839d 100644 --- a/frontend/src/api/attendee.client.ts +++ b/frontend/src/api/attendee.client.ts @@ -8,6 +8,7 @@ export interface EditAttendeeRequest { first_name: string; last_name: string; email: string; + notes?: string; product_id?: IdParam; product_price_id?: IdParam; status?: string; diff --git a/frontend/src/components/common/AttendeeDetails/index.tsx b/frontend/src/components/common/AttendeeDetails/index.tsx index 57066d30f5..0ecc72de23 100644 --- a/frontend/src/components/common/AttendeeDetails/index.tsx +++ b/frontend/src/components/common/AttendeeDetails/index.tsx @@ -7,7 +7,7 @@ import {getLocaleName, SupportedLocales} from "../../../locales.ts"; export const AttendeeDetails = ({attendee}: { attendee: Attendee }) => { return ( -
+
{t`Name`} @@ -29,7 +29,7 @@ export const AttendeeDetails = ({attendee}: { attendee: Attendee }) => { {t`Status`}
- {attendee.status} + {attendee.status === 'ACTIVE' ? {t`Active`} : {t`Canceled`}}
diff --git a/frontend/src/components/common/AttendeeTable/index.tsx b/frontend/src/components/common/AttendeeTable/index.tsx index 5fd2f12b31..acc6b6b8e7 100644 --- a/frontend/src/components/common/AttendeeTable/index.tsx +++ b/frontend/src/components/common/AttendeeTable/index.tsx @@ -1,6 +1,6 @@ import {Anchor, Avatar, Badge, Button, Table as MantineTable,} from '@mantine/core'; import {Attendee, MessageType} from "../../../types.ts"; -import {IconEye, IconMailForward, IconPencil, IconPlus, IconSend, IconTrash} from "@tabler/icons-react"; +import {IconMailForward, IconPlus, IconSend, IconTrash, IconUserCog} from "@tabler/icons-react"; import {getInitials, getProductFromEvent} from "../../../utilites/helpers.ts"; import {Table, TableHead} from "../Table"; import {useDisclosure} from "@mantine/hooks"; @@ -8,7 +8,6 @@ import {SendMessageModal} from "../../modals/SendMessageModal"; import {useState} from "react"; import {NoResultsSplash} from "../NoResultsSplash"; import {useParams} from "react-router-dom"; -import {EditAttendeeModal} from "../../modals/EditAttendeeModal"; import {useGetEvent} from "../../../queries/useGetEvent.ts"; import Truncate from "../Truncate"; import {notifications} from "@mantine/notifications"; @@ -17,7 +16,7 @@ import {showError, showSuccess} from "../../../utilites/notifications.tsx"; import {t, Trans} from "@lingui/macro"; import {confirmationDialog} from "../../../utilites/confirmationDialog.tsx"; import {useResendAttendeeTicket} from "../../../mutations/useResendAttendeeTicket.ts"; -import {ViewAttendeeModal} from "../../modals/ViewAttendeeModal"; +import {ManageAttendeeModal} from "../../modals/ManageAttendeeModal"; import {ActionMenu} from '../ActionMenu'; interface AttendeeTableProps { @@ -28,7 +27,6 @@ interface AttendeeTableProps { export const AttendeeTable = ({attendees, openCreateModal}: AttendeeTableProps) => { const {eventId} = useParams(); const [isMessageModalOpen, messageModal] = useDisclosure(false); - const [isEditModalOpen, editModal] = useDisclosure(false); const [isViewModalOpem, viewModalOpen] = useDisclosure(false); const [selectedAttendee, setSelectedAttendee] = useState(); const {data: event} = useGetEvent(eventId); @@ -161,8 +159,8 @@ export const AttendeeTable = ({attendees, openCreateModal}: AttendeeTableProps) label: t`Actions`, items: [ { - label: t`View attendee`, - icon: , + label: t`Manage attendee`, + icon: , onClick: () => handleModalClick(attendee, viewModalOpen), }, { @@ -170,11 +168,6 @@ export const AttendeeTable = ({attendees, openCreateModal}: AttendeeTableProps) icon: , onClick: () => handleModalClick(attendee, messageModal), }, - { - label: t`Edit attendee`, - icon: , - onClick: () => handleModalClick(attendee, editModal), - }, { label: t`Resend ticket email`, icon: , @@ -207,12 +200,7 @@ export const AttendeeTable = ({attendees, openCreateModal}: AttendeeTableProps) attendeeId={selectedAttendee.id} messageType={MessageType.Attendee} />} - {(selectedAttendee?.id && isEditModalOpen) && } - - {(selectedAttendee?.id && isViewModalOpem) && } diff --git a/frontend/src/components/forms/ProductCategoryForm/index.tsx b/frontend/src/components/forms/ProductCategoryForm/index.tsx index 49d40549e8..b26a93eaa4 100644 --- a/frontend/src/components/forms/ProductCategoryForm/index.tsx +++ b/frontend/src/components/forms/ProductCategoryForm/index.tsx @@ -21,7 +21,6 @@ export const ProductCategoryForm = ({form}: ProductCategoryFormProps) => { diff --git a/frontend/src/components/modals/EditAttendeeModal/index.tsx b/frontend/src/components/modals/EditAttendeeModal/index.tsx deleted file mode 100644 index 9e67208e5d..0000000000 --- a/frontend/src/components/modals/EditAttendeeModal/index.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import {Modal} from "../../common/Modal"; -import {GenericModalProps, ProductCategory, ProductType} from "../../../types.ts"; -import {Button} from "../../common/Button"; -import {useParams} from "react-router-dom"; -import {useFormErrorResponseHandler} from "../../../hooks/useFormErrorResponseHandler.tsx"; -import {useForm} from "@mantine/form"; -import {LoadingOverlay, TextInput} from "@mantine/core"; -import {EditAttendeeRequest} from "../../../api/attendee.client.ts"; -import {useGetAttendee} from "../../../queries/useGetAttendee.ts"; -import {useEffect} from "react"; -import {useUpdateAttendee} from "../../../mutations/useUpdateAttendee.ts"; -import {showSuccess} from "../../../utilites/notifications.tsx"; -import {useGetEvent} from "../../../queries/useGetEvent.ts"; -import {t} from "@lingui/macro"; -import {InputGroup} from "../../common/InputGroup"; -import {ProductSelector} from "../../common/ProductSelector"; - -interface EditAttendeeModalProps extends GenericModalProps { - attendeeId: number; -} - -export const EditAttendeeModal = ({onClose, attendeeId}: EditAttendeeModalProps) => { - const {eventId} = useParams(); - const errorHandler = useFormErrorResponseHandler(); - const {data: attendee, isFetched} = useGetAttendee(eventId, attendeeId); - const {data: event} = useGetEvent(eventId); - const mutation = useUpdateAttendee(); - const form = useForm({ - initialValues: { - first_name: '', - last_name: '', - email: '', - product_id: '', - product_price_id: '', - }, - }); - - useEffect(() => { - if (!attendee) { - return; - } - - form.setValues({ - first_name: attendee.first_name, - last_name: attendee.last_name, - email: attendee.email, - product_id: String(attendee.product_id), - product_price_id: String(attendee.product_price_id), - }); - - }, [isFetched]); - - const handleSubmit = (values: EditAttendeeRequest) => { - mutation.mutate({ - attendeeId: attendeeId, - eventId: eventId, - attendeeData: values, - }, { - onSuccess: () => { - showSuccess(t`Successfully updated attendee`); - onClose(); - }, - onError: (error) => errorHandler(form, error), - }) - }; - - if (!isFetched) { - return - } - - return ( - -
- - - - - - - - {event?.product_categories && event.product_categories.length > 0 && ( - - )} - - - -
- ); -} diff --git a/frontend/src/components/modals/ViewAttendeeModal/ViewAttendeeModal.module.scss b/frontend/src/components/modals/ManageAttendeeModal/ManageAttendeeModal.module.scss similarity index 100% rename from frontend/src/components/modals/ViewAttendeeModal/ViewAttendeeModal.module.scss rename to frontend/src/components/modals/ManageAttendeeModal/ManageAttendeeModal.module.scss diff --git a/frontend/src/components/modals/ManageAttendeeModal/index.tsx b/frontend/src/components/modals/ManageAttendeeModal/index.tsx new file mode 100644 index 0000000000..d45b9bd88a --- /dev/null +++ b/frontend/src/components/modals/ManageAttendeeModal/index.tsx @@ -0,0 +1,225 @@ +import {useParams} from "react-router-dom"; +import {useGetAttendee} from "../../../queries/useGetAttendee.ts"; +import {useGetEvent} from "../../../queries/useGetEvent.ts"; +import {useGetOrder} from "../../../queries/useGetOrder.ts"; +import {useUpdateAttendee} from "../../../mutations/useUpdateAttendee.ts"; +import {useFormErrorResponseHandler} from "../../../hooks/useFormErrorResponseHandler.tsx"; +import {useForm} from "@mantine/form"; +import {Modal} from "../../common/Modal"; +import {Accordion} from "../../common/Accordion"; +import {Button} from "../../common/Button"; +import {Avatar, Badge, Box, Group, Stack, Tabs, Text, Textarea, TextInput} from "@mantine/core"; +import {IconEdit, IconNotebook, IconQuestionMark, IconReceipt, IconTicket, IconUser} from "@tabler/icons-react"; +import {LoadingMask} from "../../common/LoadingMask"; +import {AttendeeDetails} from "../../common/AttendeeDetails"; +import {OrderDetails} from "../../common/OrderDetails"; +import {QuestionAndAnswerList} from "../../common/QuestionAndAnswerList"; +import {AttendeeProduct} from "../../common/AttendeeProduct"; +import {getInitials} from "../../../utilites/helpers.ts"; +import {t} from "@lingui/macro"; +import classes from './ManageAttendeeModal.module.scss'; +import {useEffect, useState} from "react"; +import {showSuccess} from "../../../utilites/notifications.tsx"; +import {ProductSelector} from "../../common/ProductSelector"; +import {GenericModalProps, IdParam, ProductCategory, ProductType, QuestionAnswer} from "../../../types.ts"; +import {InputGroup} from "../../common/InputGroup"; +import {InputLabelWithHelp} from "../../common/InputLabelWithHelp"; +import {EditAttendeeRequest} from "../../../api/attendee.client.ts"; + +interface ManageAttendeeModalProps extends GenericModalProps { + onClose: () => void; + attendeeId: IdParam; +} + +export const ManageAttendeeModal = ({onClose, attendeeId}: ManageAttendeeModalProps) => { + const {eventId} = useParams(); + const {data: attendee} = useGetAttendee(eventId, attendeeId); + const {data: order} = useGetOrder(eventId, attendee?.order_id); + const {data: event} = useGetEvent(eventId); + const errorHandler = useFormErrorResponseHandler(); + const mutation = useUpdateAttendee(); + + const form = useForm({ + initialValues: { + first_name: "", + last_name: "", + email: "", + notes: "", + product_id: "", + product_price_id: "", + }, + }); + + const [activeTab, setActiveTab] = useState("view"); + + useEffect(() => { + if (attendee) { + form.initialize({ + first_name: attendee.first_name, + last_name: attendee.last_name, + email: attendee.email, + notes: attendee.notes || "", + product_id: String(attendee.product_id), + product_price_id: String(attendee.product_price_id), + }); + } + }, [attendee]); + + if (!attendee || !order || !event) { + return ; + } + + const handleSubmit = (values: EditAttendeeRequest) => { + mutation.mutate( + { + attendeeId, + eventId, + attendeeData: values, + }, + { + onSuccess: () => { + showSuccess(t`Successfully updated attendee`); + setActiveTab("view"); + }, + onError: (error) => errorHandler(form, error), + } + ); + }; + + const fullName = `${attendee.first_name} ${attendee.last_name}`; + const hasQuestions = attendee.question_answers && attendee.question_answers.length > 0; + + const detailsTab = ( +
+ + + + + + + {event?.product_categories && event.product_categories.length > 0 && ( + + )} + +