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;
+ }
+ }
+ }
+}