diff --git a/README.md b/README.md index 5dc8437a2..e9617f536 100644 --- a/README.md +++ b/README.md @@ -164,17 +164,6 @@ managed option with zero setup, automatic updates, and managed infrastructure.
-## Documentation - -| Resource | Link | -|:----------------|:----------------------------------------------------------------------------------------------| -| Getting Started | [hi.events/docs/getting-started](https://hi.events/docs/getting-started?utm_source=gh-readme) | -| Configuration | [hi.events/docs/configuration](https://hi.events/docs/configuration?utm_source=gh-readme) | -| API Reference | [hi.events/docs/api](https://hi.events/docs/api?utm_source=gh-readme) | -| Webhooks | [hi.events/docs/webhooks](https://hi.events/docs/webhooks?utm_source=gh-readme) | - -
- ## Contributing We welcome contributions. See the [contributing guide](CONTRIBUTING.md) for details. diff --git a/backend/app/Services/Domain/Message/MessagingEligibilityService.php b/backend/app/Services/Domain/Message/MessagingEligibilityService.php index afdfa4a53..5a5b1df3b 100644 --- a/backend/app/Services/Domain/Message/MessagingEligibilityService.php +++ b/backend/app/Services/Domain/Message/MessagingEligibilityService.php @@ -30,12 +30,19 @@ public function __construct( public function checkEligibility(int $accountId, int $eventId): ?MessagingEligibilityFailureDTO { - $failures = []; - $account = $this->accountRepository ->loadRelation(AccountStripePlatformDomainObject::class) ->findById($accountId); + $tier = $this->getAccountMessagingTier($account->getAccountMessagingTierId()); + + // Trusted and Premium tiers bypass eligibility checks + if ($tier->getName() !== self::UNTRUSTED_TIER_NAME) { + return null; + } + + $failures = []; + if (!$account->isStripeSetupComplete()) { $failures[] = MessagingEligibilityFailureEnum::STRIPE_NOT_CONNECTED; } @@ -69,11 +76,11 @@ public function checkTierLimits(int $accountId, int $recipientCount, string $mes $messagesInLast24h = $this->messageRepository->countMessagesInLast24Hours($accountId); if ($messagesInLast24h >= $tier->getMaxMessagesPer24h()) { - // $violations[] = MessagingTierViolationEnum::MESSAGE_LIMIT_EXCEEDED; + $violations[] = MessagingTierViolationEnum::MESSAGE_LIMIT_EXCEEDED; } if ($recipientCount > $tier->getMaxRecipientsPerMessage()) { - // $violations[] = MessagingTierViolationEnum::RECIPIENT_LIMIT_EXCEEDED; + $violations[] = MessagingTierViolationEnum::RECIPIENT_LIMIT_EXCEEDED; } if (!$tier->getLinksAllowed() && $this->containsLinks($messageContent)) { diff --git a/frontend/src/components/common/AdminEventsTable/AdminEventsTable.module.scss b/frontend/src/components/common/AdminEventsTable/AdminEventsTable.module.scss deleted file mode 100644 index b378c7b60..000000000 --- a/frontend/src/components/common/AdminEventsTable/AdminEventsTable.module.scss +++ /dev/null @@ -1,13 +0,0 @@ -.tableContainer { - overflow-x: auto; - border-radius: 8px; - border: 1px solid var(--mantine-color-gray-3); -} - -.emptyState { - display: flex; - justify-content: center; - align-items: center; - padding: 60px 20px; - text-align: center; -} diff --git a/frontend/src/components/common/AdminEventsTable/index.tsx b/frontend/src/components/common/AdminEventsTable/index.tsx index 7797f01c3..78a12feb5 100644 --- a/frontend/src/components/common/AdminEventsTable/index.tsx +++ b/frontend/src/components/common/AdminEventsTable/index.tsx @@ -3,7 +3,7 @@ import {t} from "@lingui/macro"; import {AdminEvent} from "../../../api/admin.client"; import {IconChevronDown, IconChevronUp, IconEye, IconUserCheck} from "@tabler/icons-react"; import {IdParam} from "../../../types"; -import classes from "./AdminEventsTable.module.scss"; +import tableStyles from "../../../styles/admin-table.module.scss"; interface AdminEventsTableProps { events: AdminEvent[]; @@ -18,7 +18,7 @@ interface AdminEventsTableProps { const AdminEventsTable = ({events, onSort, sortBy, sortDirection, onViewEvent, onImpersonate, isImpersonating}: AdminEventsTableProps) => { if (!events || events.length === 0) { return ( -
+
{t`No events found`}
); @@ -68,113 +68,118 @@ const AdminEventsTable = ({events, onSort, sortBy, sortDirection, onViewEvent, o }; return ( -
- - - - - - - {t`Organizer`} - - - - - - - {t`Statistics`} - {t`Status`} - {t`Actions`} - - - - {events.map((event) => ( - - - {event.title} - - - {event.organizer_name} - - - {formatDate(event.start_date)} - - - {formatDate(event.end_date)} - - - {event.statistics ? ( - - - {t`Sales:`} - {formatCurrency(event.statistics.total_gross_sales)} - - - {t`Attendees:`} - {formatNumber(event.statistics.attendees_registered)} - - - {t`Orders:`} - {formatNumber(event.statistics.orders_created)} - - - ) : ( - - - )} - - - - {event.status} - - - - - - onViewEvent?.(event)} - > - - - - - onImpersonate?.(event.user_id, event.account_id)} - disabled={isImpersonating} - > - - - - - +
+
+
+ + + + + + {t`Organizer`} + + + + + + + {t`Statistics`} + {t`Status`} + {t`Actions`} - ))} - -
+ + + {events.map((event) => ( + + + {event.title} + + + {event.organizer_name} + + + {formatDate(event.start_date)} + + + {formatDate(event.end_date)} + + + {event.statistics ? ( + + + {t`Sales:`} + {formatCurrency(event.statistics.total_gross_sales)} + + + {t`Attendees:`} + {formatNumber(event.statistics.attendees_registered)} + + + {t`Orders:`} + {formatNumber(event.statistics.orders_created)} + + + ) : ( + - + )} + + + + {event.status} + + + + + + onViewEvent?.(event)} + > + + + + + onImpersonate?.(event.user_id, event.account_id)} + disabled={isImpersonating} + > + + + + + + + ))} + + +
); }; diff --git a/frontend/src/components/common/AdminOrdersTable/AdminOrdersTable.module.scss b/frontend/src/components/common/AdminOrdersTable/AdminOrdersTable.module.scss deleted file mode 100644 index b378c7b60..000000000 --- a/frontend/src/components/common/AdminOrdersTable/AdminOrdersTable.module.scss +++ /dev/null @@ -1,13 +0,0 @@ -.tableContainer { - overflow-x: auto; - border-radius: 8px; - border: 1px solid var(--mantine-color-gray-3); -} - -.emptyState { - display: flex; - justify-content: center; - align-items: center; - padding: 60px 20px; - text-align: center; -} diff --git a/frontend/src/components/common/AdminOrdersTable/index.tsx b/frontend/src/components/common/AdminOrdersTable/index.tsx index 425d19b1d..7a05f1e91 100644 --- a/frontend/src/components/common/AdminOrdersTable/index.tsx +++ b/frontend/src/components/common/AdminOrdersTable/index.tsx @@ -4,7 +4,7 @@ import {AdminOrder} from "../../../api/admin.client"; import {IconChevronDown, IconChevronUp} from "@tabler/icons-react"; import {formatCurrency} from "../../../utilites/currency"; import {prettyDate} from "../../../utilites/dates"; -import classes from "./AdminOrdersTable.module.scss"; +import tableStyles from "../../../styles/admin-table.module.scss"; interface AdminOrdersTableProps { orders: AdminOrder[]; @@ -16,7 +16,7 @@ interface AdminOrdersTableProps { const AdminOrdersTable = ({orders, onSort, sortBy, sortDirection}: AdminOrdersTableProps) => { if (!orders || orders.length === 0) { return ( -
+
{t`No orders found`}
); @@ -51,85 +51,90 @@ const AdminOrdersTable = ({orders, onSort, sortBy, sortDirection}: AdminOrdersTa }; return ( -
- - - - - - - {t`Account`} - {t`Customer`} - {t`Event`} - - - - {t`Tax`} - {t`Status`} - - - - - - - {orders.map((order) => ( - - - #{order.short_id} - - - {order.account_name} - - -
- {order.first_name} {order.last_name} - {order.email} -
-
- - {order.event_title} - - - {formatCurrency(order.total_gross, order.currency)} - - - {formatCurrency(order.total_tax, order.currency)} - - - - - {order.status} - - - - - {prettyDate(order.created_at, 'UTC')} - +
+
+
+ + + + + + {t`Account`} + {t`Customer`} + {t`Event`} + + + + {t`Tax`} + {t`Status`} + + + - ))} - -
+ + + {orders.map((order) => ( + + + #{order.short_id} + + + {order.account_name} + + +
+ {order.first_name} {order.last_name} + {order.email} +
+
+ + {order.event_title} + + + {formatCurrency(order.total_gross, order.currency)} + + + {formatCurrency(order.total_tax, order.currency)} + + + + + {order.status} + + + + + {prettyDate(order.created_at, 'UTC')} + +
+ ))} +
+ +
); }; diff --git a/frontend/src/components/routes/admin/Attribution/Attribution.module.scss b/frontend/src/components/routes/admin/Attribution/Attribution.module.scss deleted file mode 100644 index df6cd7faf..000000000 --- a/frontend/src/components/routes/admin/Attribution/Attribution.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.tableContainer { - overflow-x: auto; -} diff --git a/frontend/src/components/routes/admin/Attribution/index.tsx b/frontend/src/components/routes/admin/Attribution/index.tsx index 6bb416872..89369183f 100644 --- a/frontend/src/components/routes/admin/Attribution/index.tsx +++ b/frontend/src/components/routes/admin/Attribution/index.tsx @@ -3,7 +3,7 @@ import {t, Trans} from "@lingui/macro"; import {useGetUtmAttributionStats} from "../../../../queries/useGetUtmAttributionStats"; import {useState} from "react"; import {formatCurrency} from "../../../../utilites/currency"; -import classes from "./Attribution.module.scss"; +import tableStyles from "../../../../styles/admin-table.module.scss"; const Attribution = () => { const [groupBy, setGroupBy] = useState<'source' | 'campaign' | 'medium' | 'source_type'>('source'); @@ -115,33 +115,31 @@ const Attribution = () => { + ) : stats.length === 0 ? ( +
+ + No attribution data found + +
) : ( - -
- - - - {t`Attribution Value`} - {t`Accounts`} - {t`Events`} - {t`Live Events`} - {t`Stripe Connected`} - {t`Verified`} - {t`Revenue`} - {t`Orders`} - - - - {stats.length === 0 ? ( + <> +
+
+
+ - - - No attribution data found - - + {t`Attribution Value`} + {t`Accounts`} + {t`Events`} + {t`Live Events`} + {t`Stripe Connected`} + {t`Verified`} + {t`Revenue`} + {t`Orders`} - ) : ( - stats.map((stat, index) => ( + + + {stats.map((stat, index) => ( @@ -170,10 +168,10 @@ const Attribution = () => { {stat.total_orders.toLocaleString()} - )) - )} - -
+ ))} + + +
{totalPages > 1 && ( @@ -185,7 +183,7 @@ const Attribution = () => { /> )} -
+ )} diff --git a/frontend/src/components/routes/admin/FailedJobs/index.tsx b/frontend/src/components/routes/admin/FailedJobs/index.tsx index ddaec3cbc..3e100be3b 100644 --- a/frontend/src/components/routes/admin/FailedJobs/index.tsx +++ b/frontend/src/components/routes/admin/FailedJobs/index.tsx @@ -11,6 +11,8 @@ import {showError, showSuccess} from "../../../../utilites/notifications"; import {AdminFailedJob} from "../../../../api/admin.client"; import {useDisclosure} from "@mantine/hooks"; import {relativeDate} from "../../../../utilites/dates"; +import tableStyles from "../../../../styles/admin-table.module.scss"; +import {IdParam} from "../../../../types"; const FailedJobs = () => { const [page, setPage] = useState(1); @@ -44,7 +46,8 @@ const FailedJobs = () => { openDetailModal(); }; - const handleRetryJob = (jobId: number | string) => { + const handleRetryJob = (jobId: IdParam) => { + if (!jobId) return; retryJobMutation.mutate(jobId, { onSuccess: () => { showSuccess(t`Job queued for retry`); @@ -55,7 +58,8 @@ const FailedJobs = () => { }); }; - const handleDeleteJob = (jobId: number | string) => { + const handleDeleteJob = (jobId: IdParam) => { + if (!jobId) return; deleteJobMutation.mutate(jobId, { onSuccess: () => { showSuccess(t`Job deleted`); @@ -147,73 +151,79 @@ const FailedJobs = () => { ) : totalJobs === 0 ? ( - {t`No failed jobs`} +
+ {t`No failed jobs`} +
) : ( - - - - {t`Job`} - {t`Queue`} - {t`Failed At`} - {t`Exception`} - {t`Actions`} - - - - {jobsData?.data?.map((job) => ( - - - {job.job_name} - {job.uuid} - - - {job.queue} - - - {relativeDate(job.failed_at)} - - - - {job.exception_summary} - - - - - - handleViewDetails(job)} - > - - - - - handleRetryJob(job.id)} - loading={retryJobMutation.isPending} - > - - - - - handleDeleteJob(job.id)} - loading={deleteJobMutation.isPending} - > - - - - - - - ))} - -
+
+
+ + + + {t`Job`} + {t`Queue`} + {t`Failed At`} + {t`Exception`} + {t`Actions`} + + + + {jobsData?.data?.map((job) => ( + + + {job.job_name} + {job.uuid} + + + {job.queue} + + + {relativeDate(job.failed_at)} + + + + {job.exception_summary} + + + + + + handleViewDetails(job)} + > + + + + + handleRetryJob(job.id)} + loading={retryJobMutation.isPending} + > + + + + + handleDeleteJob(job.id)} + loading={deleteJobMutation.isPending} + > + + + + + + + ))} + +
+
+
)} {jobsData?.meta && jobsData.meta.last_page > 1 && ( diff --git a/frontend/src/components/routes/admin/Messages/index.tsx b/frontend/src/components/routes/admin/Messages/index.tsx index 4c5452a73..e5e7cb5c9 100644 --- a/frontend/src/components/routes/admin/Messages/index.tsx +++ b/frontend/src/components/routes/admin/Messages/index.tsx @@ -9,6 +9,7 @@ import {useDisclosure} from "@mantine/hooks"; import {AdminMessage} from "../../../../api/admin.client"; import {showSuccess} from "../../../../utilites/notifications"; import {IdParam} from "../../../../types"; +import tableStyles from "../../../../styles/admin-table.module.scss"; const Messages = () => { const [page, setPage] = useState(1); @@ -143,98 +144,104 @@ const Messages = () => { ) : totalMessages === 0 ? ( - {t`No messages found`} +
+ {t`No messages found`} +
) : ( - - - - {t`Subject`} - {t`Event`} - {t`Account`} - {t`Type`} - {t`Recipients`} - {t`Status`} - {t`Sent By`} - {t`Created`} - - - - - {messagesData?.data?.map((message) => ( - - - - - - {message.subject} - - - - - - {message.event_title} - - - - - {message.account_name} - - - - - {getTypeLabel(message.type)} - - - - - - - {message.recipients_count} - - - - - - {message.status} - - - - - {message.sent_by || '-'} - - - - {relativeDate(message.created_at)} - - - - - handleViewMessage(message)} - > - - - - {message.status === 'PENDING_REVIEW' && ( - - handleApprove(message.id)} - loading={approveMutation.isPending} - > - - - - )} - - - - ))} - -
+
+
+ + + + {t`Subject`} + {t`Event`} + {t`Account`} + {t`Type`} + {t`Recipients`} + {t`Status`} + {t`Sent By`} + {t`Created`} + + + + + {messagesData?.data?.map((message) => ( + + + + + + {message.subject} + + + + + + {message.event_title} + + + + + {message.account_name} + + + + + {getTypeLabel(message.type)} + + + + + + + {message.recipients_count} + + + + + + {message.status} + + + + + {message.sent_by || '-'} + + + + {relativeDate(message.created_at)} + + + + + handleViewMessage(message)} + > + + + + {message.status === 'PENDING_REVIEW' && ( + + handleApprove(message.id)} + loading={approveMutation.isPending} + > + + + + )} + + + + ))} + +
+
+
)} {messagesData?.meta && messagesData.meta.last_page > 1 && ( @@ -278,7 +285,7 @@ const Messages = () => {
{t`Status`} - + {selectedMessage.status}
@@ -311,7 +318,7 @@ const Messages = () => { {t`Eligibility Failures`} {selectedMessage.eligibility_failures.map((failure) => ( - + {getEligibilityFailureLabel(failure)} ))} diff --git a/frontend/src/styles/admin-table.module.scss b/frontend/src/styles/admin-table.module.scss new file mode 100644 index 000000000..5eee36694 --- /dev/null +++ b/frontend/src/styles/admin-table.module.scss @@ -0,0 +1,112 @@ +@use "./mixins" as *; + +.tableWrapper { + background: var(--mantine-color-body); + border: 1px solid var(--mantine-color-gray-2); + border-radius: var(--mantine-radius-md); + overflow: hidden; + + [data-mantine-color-scheme="dark"] & { + border-color: var(--mantine-color-dark-4); + } +} + +.tableScroll { + overflow-x: auto; + @include scrollbar(); + min-width: 0; +} + +.table { + :global { + thead tr th { + padding: 12px 16px; + font-weight: 500; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--mantine-color-gray-6); + background: var(--mantine-color-gray-0); + border-bottom: 1px solid var(--mantine-color-gray-2); + white-space: nowrap; + + [data-mantine-color-scheme="dark"] & { + background: var(--mantine-color-dark-6); + border-bottom-color: var(--mantine-color-dark-4); + color: var(--mantine-color-dark-2); + } + } + + tbody tr td { + padding: 12px 16px; + vertical-align: middle; + border-bottom: 1px solid var(--mantine-color-gray-1); + + [data-mantine-color-scheme="dark"] & { + border-bottom-color: var(--mantine-color-dark-5); + } + } + + tbody tr:last-child td { + border-bottom: none; + } + + tbody tr:hover td { + background: rgba(0, 0, 0, 0.02); + + [data-mantine-color-scheme="dark"] & { + background: rgba(255, 255, 255, 0.02); + } + } + } +} + +.sortButton { + font-weight: 500; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--mantine-color-gray-6); + padding: 0; + height: auto; + + [data-mantine-color-scheme="dark"] & { + color: var(--mantine-color-dark-2); + } + + &:hover { + background: transparent; + color: var(--mantine-color-gray-8); + + [data-mantine-color-scheme="dark"] & { + color: var(--mantine-color-gray-4); + } + } +} + +.emptyState { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 48px 24px; + text-align: center; + background: var(--mantine-color-body); + border: 1px solid var(--mantine-color-gray-2); + border-radius: var(--mantine-radius-md); + + [data-mantine-color-scheme="dark"] & { + border-color: var(--mantine-color-dark-4); + } +} + +@include respond-below('md') { + .table { + :global { + thead tr th, + tbody tr td { + padding: 10px 12px; + } + } + } +}