Account.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import {
  2. InboxOutlined,
  3. ReloadOutlined,
  4. SettingOutlined
  5. } from '@ant-design/icons';
  6. import {
  7. Alert,
  8. Button,
  9. Card,
  10. Descriptions,
  11. List,
  12. Skeleton,
  13. Space,
  14. Table,
  15. Tag,
  16. Typography
  17. } from 'antd';
  18. import type { ColumnsType } from 'antd/es/table';
  19. import { useCallback, useEffect, useMemo, useState } from 'react';
  20. import { useNavigate } from 'react-router-dom';
  21. import { EmptyState } from '../components/common/EmptyState';
  22. import { PageHeader } from '../components/common/PageHeader';
  23. import { SectionCard } from '../components/common/SectionCard';
  24. import { MailboxPermissionTags } from '../components/mailbox/MailboxPermissionTags';
  25. import { useAppContext } from '../frontend/app-context';
  26. import { useI18n } from '../frontend/i18n/react';
  27. import { api } from '../frontend/services/api';
  28. import type { InboundMailbox, MailboxAccess } from '../frontend/types';
  29. import { useMediaQuery } from '../frontend/use-media-query';
  30. export default function Account() {
  31. const { user } = useAppContext();
  32. const { t } = useI18n();
  33. const navigate = useNavigate();
  34. const isTableView = useMediaQuery('(min-width: 768px)');
  35. const [mailboxes, setMailboxes] = useState<InboundMailbox[]>([]);
  36. const [loading, setLoading] = useState(true);
  37. const [loadError, setLoadError] = useState('');
  38. const loadMailboxes = useCallback(async () => {
  39. setLoading(true);
  40. setLoadError('');
  41. try {
  42. const result = await api.inboundMailboxes('effective');
  43. setMailboxes(result.mailboxes || []);
  44. } catch (error) {
  45. setLoadError(error instanceof Error ? error.message : t('common.error'));
  46. } finally {
  47. setLoading(false);
  48. }
  49. }, [t]);
  50. useEffect(() => {
  51. void loadMailboxes();
  52. }, [loadMailboxes]);
  53. const counts = useMemo(() => ({
  54. owned: mailboxes.filter((mailbox) => mailboxAccess(mailbox).type === 'owner').length,
  55. assigned: mailboxes.filter((mailbox) => mailboxAccess(mailbox).type === 'assigned').length
  56. }), [mailboxes]);
  57. function openInbox(mailbox: InboundMailbox) {
  58. navigate(`/inbox?mailboxId=${mailbox.id}&folder=INBOX`);
  59. }
  60. function manageMailbox(mailbox: InboundMailbox) {
  61. navigate(`/inbox?workspace=routing&mailboxId=${mailbox.id}`);
  62. }
  63. const columns: ColumnsType<InboundMailbox> = [
  64. {
  65. title: t('inbox.mailboxAddress'),
  66. render: (_, mailbox) => (
  67. <Space direction="vertical" size={0}>
  68. <Typography.Text strong>{mailbox.address}</Typography.Text>
  69. {mailbox.displayName ? <Typography.Text type="secondary">{mailbox.displayName}</Typography.Text> : null}
  70. </Space>
  71. )
  72. },
  73. {
  74. title: t('account.accessSource'),
  75. width: 160,
  76. render: (_, mailbox) => <AccessSource mailbox={mailbox} />
  77. },
  78. {
  79. title: t('account.permissions'),
  80. width: 280,
  81. render: (_, mailbox) => <MailboxPermissionTags permissions={mailboxAccess(mailbox).permissions} />
  82. },
  83. {
  84. title: t('common.actions'),
  85. width: 250,
  86. render: (_, mailbox) => (
  87. <Space wrap>
  88. {mailboxAccess(mailbox).permissions.receive ? (
  89. <Button aria-label={`${t('account.openInbox')} · ${mailbox.address}`} icon={<InboxOutlined />} style={{ minHeight: 44 }} onClick={() => openInbox(mailbox)}>
  90. {t('account.openInbox')}
  91. </Button>
  92. ) : null}
  93. {mailboxAccess(mailbox).type === 'owner' ? (
  94. <Button aria-label={`${t('account.manageMailbox')} · ${mailbox.address}`} icon={<SettingOutlined />} style={{ minHeight: 44 }} onClick={() => manageMailbox(mailbox)}>
  95. {t('account.manageMailbox')}
  96. </Button>
  97. ) : null}
  98. </Space>
  99. )
  100. }
  101. ];
  102. return (
  103. <Space direction="vertical" size={20} className="full-width account-page">
  104. <PageHeader title={t('account.title')} subtitle={t('account.subtitle')} />
  105. <SectionCard title={t('account.summary')}>
  106. <Descriptions column={isTableView ? 2 : 1} bordered size="small">
  107. <Descriptions.Item label={t('auth.username')}>{user?.username || '—'}</Descriptions.Item>
  108. <Descriptions.Item label={t('auth.email')}>{user?.email || '—'}</Descriptions.Item>
  109. <Descriptions.Item label={t('account.role')}>{user?.role || '—'}</Descriptions.Item>
  110. <Descriptions.Item label={t('common.status')}>{user?.status || '—'}</Descriptions.Item>
  111. </Descriptions>
  112. </SectionCard>
  113. {loadError ? (
  114. <Alert
  115. type="error"
  116. showIcon
  117. message={loadError}
  118. action={<Button icon={<ReloadOutlined />} onClick={() => void loadMailboxes()}>{t('common.refresh')}</Button>}
  119. />
  120. ) : null}
  121. <SectionCard
  122. title={t('account.mailboxAccess')}
  123. extra={<Space wrap><Tag>{t('account.owned')} {counts.owned}</Tag><Tag>{t('account.assigned')} {counts.assigned}</Tag></Space>}
  124. >
  125. {loading && !mailboxes.length ? <Skeleton active paragraph={{ rows: 7 }} /> : mailboxes.length ? (
  126. isTableView ? (
  127. <Table rowKey="id" columns={columns} dataSource={mailboxes} scroll={{ x: 900 }} />
  128. ) : (
  129. <List
  130. dataSource={mailboxes}
  131. renderItem={(mailbox) => (
  132. <List.Item>
  133. <Card size="small" title={mailbox.address} className="full-width account-mailbox-card">
  134. <Space direction="vertical" size={12} className="full-width">
  135. <AccessSource mailbox={mailbox} />
  136. <MailboxPermissionTags permissions={mailboxAccess(mailbox).permissions} />
  137. {mailboxAccess(mailbox).permissions.receive ? (
  138. <Button block aria-label={`${t('account.openInbox')} · ${mailbox.address}`} icon={<InboxOutlined />} style={{ minHeight: 44 }} onClick={() => openInbox(mailbox)}>
  139. {t('account.openInbox')}
  140. </Button>
  141. ) : null}
  142. {mailboxAccess(mailbox).type === 'owner' ? (
  143. <Button block aria-label={`${t('account.manageMailbox')} · ${mailbox.address}`} icon={<SettingOutlined />} style={{ minHeight: 44 }} onClick={() => manageMailbox(mailbox)}>
  144. {t('account.manageMailbox')}
  145. </Button>
  146. ) : null}
  147. </Space>
  148. </Card>
  149. </List.Item>
  150. )}
  151. />
  152. )
  153. ) : <EmptyState description={t('account.noMailboxes')} action={<Button onClick={() => navigate('/inbox?workspace=routing')}>{t('inbox.createMailbox')}</Button>} />}
  154. </SectionCard>
  155. </Space>
  156. );
  157. }
  158. function AccessSource({ mailbox }: { mailbox: InboundMailbox }) {
  159. const { t } = useI18n();
  160. const access = mailboxAccess(mailbox);
  161. if (access.type === 'owner') return <Tag color="blue">{t('account.ownedMailbox')}</Tag>;
  162. if (access.type === 'admin') return <Tag color="gold">{t('account.adminAccess')}</Tag>;
  163. return <Tag color="purple">{t('account.adminAssigned')}</Tag>;
  164. }
  165. function mailboxAccess(mailbox: InboundMailbox): MailboxAccess {
  166. return mailbox.access || {
  167. type: 'owner',
  168. permissions: { view: true, receive: true, send: true }
  169. };
  170. }