@varshylinc/team-management 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/actions.d.ts +20 -0
- package/dist/client/actions.d.ts.map +1 -0
- package/dist/client/actions.js +23 -0
- package/dist/client/actions.js.map +1 -0
- package/dist/client/api.d.ts +74 -0
- package/dist/client/api.d.ts.map +1 -0
- package/dist/client/api.js +245 -0
- package/dist/client/api.js.map +1 -0
- package/dist/client/components/AddMemberForm.d.ts +12 -0
- package/dist/client/components/AddMemberForm.d.ts.map +1 -0
- package/dist/client/components/AddMemberForm.js +37 -0
- package/dist/client/components/AddMemberForm.js.map +1 -0
- package/dist/client/components/AuditEventRow.d.ts +7 -0
- package/dist/client/components/AuditEventRow.d.ts.map +1 -0
- package/dist/client/components/AuditEventRow.js +20 -0
- package/dist/client/components/AuditEventRow.js.map +1 -0
- package/dist/client/components/CascadePreview.d.ts +11 -0
- package/dist/client/components/CascadePreview.d.ts.map +1 -0
- package/dist/client/components/CascadePreview.js +7 -0
- package/dist/client/components/CascadePreview.js.map +1 -0
- package/dist/client/components/DangerZoneCard.d.ts +10 -0
- package/dist/client/components/DangerZoneCard.d.ts.map +1 -0
- package/dist/client/components/DangerZoneCard.js +28 -0
- package/dist/client/components/DangerZoneCard.js.map +1 -0
- package/dist/client/components/InvitationCodeDisplay.d.ts +7 -0
- package/dist/client/components/InvitationCodeDisplay.d.ts.map +1 -0
- package/dist/client/components/InvitationCodeDisplay.js +26 -0
- package/dist/client/components/InvitationCodeDisplay.js.map +1 -0
- package/dist/client/components/InviteForm.d.ts +7 -0
- package/dist/client/components/InviteForm.d.ts.map +1 -0
- package/dist/client/components/InviteForm.js +35 -0
- package/dist/client/components/InviteForm.js.map +1 -0
- package/dist/client/components/MemberRow.d.ts +10 -0
- package/dist/client/components/MemberRow.d.ts.map +1 -0
- package/dist/client/components/MemberRow.js +17 -0
- package/dist/client/components/MemberRow.js.map +1 -0
- package/dist/client/components/OrgPeopleRoster.d.ts +11 -0
- package/dist/client/components/OrgPeopleRoster.d.ts.map +1 -0
- package/dist/client/components/OrgPeopleRoster.js +13 -0
- package/dist/client/components/OrgPeopleRoster.js.map +1 -0
- package/dist/client/components/PendingTransferBanner.d.ts +10 -0
- package/dist/client/components/PendingTransferBanner.d.ts.map +1 -0
- package/dist/client/components/PendingTransferBanner.js +48 -0
- package/dist/client/components/PendingTransferBanner.js.map +1 -0
- package/dist/client/components/PlaceholderCard.d.ts +7 -0
- package/dist/client/components/PlaceholderCard.d.ts.map +1 -0
- package/dist/client/components/PlaceholderCard.js +15 -0
- package/dist/client/components/PlaceholderCard.js.map +1 -0
- package/dist/client/components/RoleBadge.d.ts +5 -0
- package/dist/client/components/RoleBadge.d.ts.map +1 -0
- package/dist/client/components/RoleBadge.js +17 -0
- package/dist/client/components/RoleBadge.js.map +1 -0
- package/dist/client/components/RoleSelect.d.ts +10 -0
- package/dist/client/components/RoleSelect.d.ts.map +1 -0
- package/dist/client/components/RoleSelect.js +12 -0
- package/dist/client/components/RoleSelect.js.map +1 -0
- package/dist/client/components/SeatUsagePanel.d.ts +6 -0
- package/dist/client/components/SeatUsagePanel.d.ts.map +1 -0
- package/dist/client/components/SeatUsagePanel.js +13 -0
- package/dist/client/components/SeatUsagePanel.js.map +1 -0
- package/dist/client/hooks/useCurrentMembership.d.ts +8 -0
- package/dist/client/hooks/useCurrentMembership.d.ts.map +1 -0
- package/dist/client/hooks/useCurrentMembership.js +20 -0
- package/dist/client/hooks/useCurrentMembership.js.map +1 -0
- package/dist/client/hooks/useMembers.d.ts +10 -0
- package/dist/client/hooks/useMembers.d.ts.map +1 -0
- package/dist/client/hooks/useMembers.js +20 -0
- package/dist/client/hooks/useMembers.js.map +1 -0
- package/dist/client/hooks/useOrgMembers.d.ts +20 -0
- package/dist/client/hooks/useOrgMembers.d.ts.map +1 -0
- package/dist/client/hooks/useOrgMembers.js +63 -0
- package/dist/client/hooks/useOrgMembers.js.map +1 -0
- package/dist/client/hooks/usePendingInvitations.d.ts +8 -0
- package/dist/client/hooks/usePendingInvitations.d.ts.map +1 -0
- package/dist/client/hooks/usePendingInvitations.js +20 -0
- package/dist/client/hooks/usePendingInvitations.js.map +1 -0
- package/dist/client/hooks/usePendingTransfer.d.ts +8 -0
- package/dist/client/hooks/usePendingTransfer.d.ts.map +1 -0
- package/dist/client/hooks/usePendingTransfer.js +23 -0
- package/dist/client/hooks/usePendingTransfer.js.map +1 -0
- package/dist/client/index.d.ts +31 -0
- package/dist/client/index.d.ts.map +1 -0
- package/{src/client/index.ts → dist/client/index.js} +6 -54
- package/dist/client/index.js.map +1 -0
- package/dist/client/pages/AuditLogPage.d.ts +6 -0
- package/dist/client/pages/AuditLogPage.d.ts.map +1 -0
- package/dist/client/pages/AuditLogPage.js +51 -0
- package/dist/client/pages/AuditLogPage.js.map +1 -0
- package/dist/client/pages/EmailChangePage.d.ts +8 -0
- package/dist/client/pages/EmailChangePage.d.ts.map +1 -0
- package/dist/client/pages/EmailChangePage.js +52 -0
- package/dist/client/pages/EmailChangePage.js.map +1 -0
- package/dist/client/pages/InvitationAcceptPage.d.ts +11 -0
- package/dist/client/pages/InvitationAcceptPage.d.ts.map +1 -0
- package/dist/client/pages/InvitationAcceptPage.js +42 -0
- package/dist/client/pages/InvitationAcceptPage.js.map +1 -0
- package/dist/client/pages/InvitationCodePage.d.ts +6 -0
- package/dist/client/pages/InvitationCodePage.d.ts.map +1 -0
- package/dist/client/pages/InvitationCodePage.js +28 -0
- package/dist/client/pages/InvitationCodePage.js.map +1 -0
- package/dist/client/pages/MembersPage.d.ts +6 -0
- package/dist/client/pages/MembersPage.d.ts.map +1 -0
- package/dist/client/pages/MembersPage.js +67 -0
- package/dist/client/pages/MembersPage.js.map +1 -0
- package/dist/client/pages/OrgPeoplePage.d.ts +14 -0
- package/dist/client/pages/OrgPeoplePage.d.ts.map +1 -0
- package/dist/client/pages/OrgPeoplePage.js +40 -0
- package/dist/client/pages/OrgPeoplePage.js.map +1 -0
- package/dist/client/pages/OrgSettingsPage.d.ts +6 -0
- package/dist/client/pages/OrgSettingsPage.d.ts.map +1 -0
- package/dist/client/pages/OrgSettingsPage.js +78 -0
- package/dist/client/pages/OrgSettingsPage.js.map +1 -0
- package/dist/client/pages/OwnershipTransferPage.d.ts +6 -0
- package/dist/client/pages/OwnershipTransferPage.d.ts.map +1 -0
- package/dist/client/pages/OwnershipTransferPage.js +68 -0
- package/dist/client/pages/OwnershipTransferPage.js.map +1 -0
- package/dist/client/pages/PasswordResetPage.d.ts +6 -0
- package/dist/client/pages/PasswordResetPage.d.ts.map +1 -0
- package/dist/client/pages/PasswordResetPage.js +34 -0
- package/dist/client/pages/PasswordResetPage.js.map +1 -0
- package/dist/client/pages/PasswordResetRequestPage.d.ts +2 -0
- package/dist/client/pages/PasswordResetRequestPage.d.ts.map +1 -0
- package/dist/client/pages/PasswordResetRequestPage.js +27 -0
- package/dist/client/pages/PasswordResetRequestPage.js.map +1 -0
- package/dist/client/pages/PlaceholderPage.d.ts +7 -0
- package/dist/client/pages/PlaceholderPage.d.ts.map +1 -0
- package/dist/client/pages/PlaceholderPage.js +16 -0
- package/dist/client/pages/PlaceholderPage.js.map +1 -0
- package/dist/client/pages/SuperAdminDashboard.d.ts +2 -0
- package/dist/client/pages/SuperAdminDashboard.d.ts.map +1 -0
- package/dist/client/pages/SuperAdminDashboard.js +123 -0
- package/dist/client/pages/SuperAdminDashboard.js.map +1 -0
- package/dist/client/theme.d.ts +12 -0
- package/dist/client/theme.d.ts.map +1 -0
- package/dist/client/theme.js +16 -0
- package/dist/client/theme.js.map +1 -0
- package/dist/client/types.d.ts +78 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/org-admin.d.ts +45 -0
- package/dist/server/org-admin.d.ts.map +1 -0
- package/dist/server/org-admin.js +63 -0
- package/dist/server/org-admin.js.map +1 -0
- package/dist/server/routes/orgs.routes.d.ts.map +1 -1
- package/dist/server/routes/orgs.routes.js +81 -12
- package/dist/server/routes/orgs.routes.js.map +1 -1
- package/dist/server/types.d.ts +2 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/package.json +18 -11
- package/.eslintrc.cjs +0 -18
- package/CHANGELOG.md +0 -159
- package/src/client/api.ts +0 -314
- package/src/client/components/AuditEventRow.tsx +0 -59
- package/src/client/components/CascadePreview.tsx +0 -36
- package/src/client/components/DangerZoneCard.tsx +0 -103
- package/src/client/components/InvitationCodeDisplay.tsx +0 -48
- package/src/client/components/InviteForm.tsx +0 -77
- package/src/client/components/MemberRow.tsx +0 -69
- package/src/client/components/PendingTransferBanner.tsx +0 -98
- package/src/client/components/PlaceholderCard.tsx +0 -26
- package/src/client/components/RoleBadge.tsx +0 -26
- package/src/client/components/RoleSelect.tsx +0 -35
- package/src/client/hooks/.gitkeep +0 -0
- package/src/client/hooks/useCurrentMembership.ts +0 -24
- package/src/client/hooks/useMembers.ts +0 -24
- package/src/client/hooks/usePendingInvitations.ts +0 -24
- package/src/client/hooks/usePendingTransfer.ts +0 -27
- package/src/client/pages/AuditLogPage.tsx +0 -164
- package/src/client/pages/EmailChangePage.tsx +0 -144
- package/src/client/pages/InvitationAcceptPage.tsx +0 -163
- package/src/client/pages/InvitationCodePage.tsx +0 -108
- package/src/client/pages/MembersPage.tsx +0 -290
- package/src/client/pages/OrgSettingsPage.tsx +0 -185
- package/src/client/pages/OwnershipTransferPage.tsx +0 -163
- package/src/client/pages/PasswordResetPage.tsx +0 -104
- package/src/client/pages/PasswordResetRequestPage.tsx +0 -71
- package/src/client/pages/PlaceholderPage.tsx +0 -20
- package/src/client/pages/SuperAdminDashboard.tsx +0 -401
- package/src/client/types.ts +0 -78
- package/src/index.ts +0 -24
- package/src/server/crypto.ts +0 -47
- package/src/server/index.ts +0 -167
- package/src/server/middleware/require-membership.ts +0 -48
- package/src/server/middleware/require-role.ts +0 -19
- package/src/server/middleware/require-super-admin.ts +0 -32
- package/src/server/migrations/0001_create_tm_schema_migrations.sql +0 -13
- package/src/server/migrations/0002_create_tm_organizations.sql +0 -14
- package/src/server/migrations/0003_create_tm_memberships.sql +0 -24
- package/src/server/migrations/0004_create_tm_invitations.sql +0 -22
- package/src/server/migrations/0005_create_tm_audit_events.sql +0 -17
- package/src/server/migrations/0006_create_tm_email_change_requests.sql +0 -13
- package/src/server/migrations/0007_create_tm_ownership_transfers.sql +0 -22
- package/src/server/migrations/0008_create_tm_super_admins.sql +0 -8
- package/src/server/migrations/0009_create_tm_password_reset_requests.sql +0 -9
- package/src/server/migrations/0010_create_tm_shared_access.sql +0 -8
- package/src/server/migrations/0011_seed_super_admin.sql +0 -15
- package/src/server/migrations/0012_create_tm_user_locks.sql +0 -7
- package/src/server/routes/admin.routes.ts +0 -208
- package/src/server/routes/audit.routes.ts +0 -93
- package/src/server/routes/health.routes.ts +0 -46
- package/src/server/routes/invitations.routes.ts +0 -252
- package/src/server/routes/me.routes.ts +0 -143
- package/src/server/routes/orgs.routes.ts +0 -428
- package/src/server/routes/transfer.routes.ts +0 -110
- package/src/server/services/.gitkeep +0 -0
- package/src/server/services/audit.service.ts +0 -49
- package/src/server/services/email-change.service.ts +0 -178
- package/src/server/services/invitations.service.ts +0 -316
- package/src/server/services/memberships.service.ts +0 -129
- package/src/server/services/organizations.service.ts +0 -110
- package/src/server/services/ownership.service.ts +0 -170
- package/src/server/services/password-reset.service.ts +0 -94
- package/src/server/services/super-admin.service.ts +0 -321
- package/src/server/sql/.gitkeep +0 -0
- package/src/server/types.ts +0 -145
- package/src/shared/types.ts +0 -24
- package/tests/integration/audit-fires.test.ts +0 -288
- package/tests/integration/cascade-preview.test.ts +0 -157
- package/tests/integration/email-change.test.ts +0 -190
- package/tests/integration/feature-flags.test.ts +0 -213
- package/tests/integration/invitations-code.test.ts +0 -218
- package/tests/integration/invitations-expiry.test.ts +0 -216
- package/tests/integration/invitations-resend.test.ts +0 -241
- package/tests/integration/invitations-revoke.test.ts +0 -226
- package/tests/integration/invitations-switch-org.test.ts +0 -156
- package/tests/integration/invitations-token.test.ts +0 -221
- package/tests/integration/migrations.test.ts +0 -119
- package/tests/integration/only-owner-protections.test.ts +0 -130
- package/tests/integration/org-lifecycle.test.ts +0 -169
- package/tests/integration/ownership-transfer-cancel.test.ts +0 -171
- package/tests/integration/ownership-transfer-expire.test.ts +0 -171
- package/tests/integration/ownership-transfer-happy.test.ts +0 -184
- package/tests/integration/ownership-transfer-locks.test.ts +0 -146
- package/tests/integration/password-reset.test.ts +0 -200
- package/tests/integration/super-admin-actions.test.ts +0 -180
- package/tests/integration/super-admin-restrictions.test.ts +0 -209
- package/tests/setup/global-setup.ts +0 -20
- package/tests/unit/adapter-shape.test.ts +0 -330
- package/tests/unit/role-permissions.test.ts +0 -236
- package/tests/unit/validation.test.ts +0 -304
- package/tsconfig.client.json +0 -13
- package/tsconfig.json +0 -12
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -13
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { useMembers } from '../hooks/useMembers.js';
|
|
3
|
-
import { usePendingInvitations } from '../hooks/usePendingInvitations.js';
|
|
4
|
-
import { usePendingTransfer } from '../hooks/usePendingTransfer.js';
|
|
5
|
-
import { useCurrentMembership } from '../hooks/useCurrentMembership.js';
|
|
6
|
-
import { MemberRow } from '../components/MemberRow.js';
|
|
7
|
-
import { InviteForm } from '../components/InviteForm.js';
|
|
8
|
-
import { PendingTransferBanner } from '../components/PendingTransferBanner.js';
|
|
9
|
-
import { RoleBadge } from '../components/RoleBadge.js';
|
|
10
|
-
import { removeMember, changeMemberRole, revokeInvitation, resendInvitation } from '../api.js';
|
|
11
|
-
import type { OrgRole } from '../types.js';
|
|
12
|
-
|
|
13
|
-
interface MembersPageProps {
|
|
14
|
-
orgId: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type ActiveTab = 'active' | 'former' | 'invitations';
|
|
18
|
-
|
|
19
|
-
export function MembersPage({ orgId }: MembersPageProps) {
|
|
20
|
-
const [tab, setTab] = useState<ActiveTab>('active');
|
|
21
|
-
const { membership } = useCurrentMembership();
|
|
22
|
-
const { members, loading: membersLoading, error: membersError, refresh: refreshMembers } = useMembers(orgId);
|
|
23
|
-
const { members: formerMembers, loading: formerLoading, error: formerError } = useMembers(orgId, { includeFormer: true });
|
|
24
|
-
const { invitations, loading: invLoading, error: invError, refresh: refreshInvitations } = usePendingInvitations(orgId);
|
|
25
|
-
const { transfer, refresh: refreshTransfer } = usePendingTransfer(orgId);
|
|
26
|
-
|
|
27
|
-
const currentRole: OrgRole = membership?.role ?? 'viewer';
|
|
28
|
-
const canManage = currentRole === 'owner' || currentRole === 'admin';
|
|
29
|
-
|
|
30
|
-
async function handleRemove(userId: number) {
|
|
31
|
-
if (!confirm('Remove this member from the organization?')) return;
|
|
32
|
-
try {
|
|
33
|
-
await removeMember(orgId, userId);
|
|
34
|
-
refreshMembers();
|
|
35
|
-
} catch (e) {
|
|
36
|
-
alert(e instanceof Error ? e.message : 'Failed to remove member');
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function handleRoleChange(userId: number, newRole: OrgRole) {
|
|
41
|
-
try {
|
|
42
|
-
await changeMemberRole(orgId, userId, newRole);
|
|
43
|
-
refreshMembers();
|
|
44
|
-
} catch (e) {
|
|
45
|
-
alert(e instanceof Error ? e.message : 'Failed to change role');
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function handleRevoke(invitationId: number) {
|
|
50
|
-
if (!confirm('Revoke this invitation?')) return;
|
|
51
|
-
try {
|
|
52
|
-
await revokeInvitation(orgId, invitationId);
|
|
53
|
-
refreshInvitations();
|
|
54
|
-
} catch (e) {
|
|
55
|
-
alert(e instanceof Error ? e.message : 'Failed to revoke invitation');
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function handleResend(invitationId: number) {
|
|
60
|
-
try {
|
|
61
|
-
await resendInvitation(orgId, invitationId);
|
|
62
|
-
alert('Invitation resent!');
|
|
63
|
-
} catch (e) {
|
|
64
|
-
alert(e instanceof Error ? e.message : 'Failed to resend invitation');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const activeMembers = members.filter((m) => !m.removed_at);
|
|
69
|
-
const formerOnly = formerMembers.filter((m) => m.removed_at);
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<div className="max-w-4xl mx-auto px-4 py-8">
|
|
73
|
-
<h1 className="text-2xl font-bold text-slate-900 mb-6">Team Members</h1>
|
|
74
|
-
|
|
75
|
-
{transfer && membership && (
|
|
76
|
-
<div className="mb-6">
|
|
77
|
-
<PendingTransferBanner
|
|
78
|
-
transfer={transfer}
|
|
79
|
-
currentUserId={membership.user_id}
|
|
80
|
-
orgId={orgId}
|
|
81
|
-
onAction={refreshTransfer}
|
|
82
|
-
/>
|
|
83
|
-
</div>
|
|
84
|
-
)}
|
|
85
|
-
|
|
86
|
-
{canManage && (
|
|
87
|
-
<div className="mb-6">
|
|
88
|
-
<InviteForm orgId={orgId} onSuccess={refreshInvitations} />
|
|
89
|
-
</div>
|
|
90
|
-
)}
|
|
91
|
-
|
|
92
|
-
{/* Tabs */}
|
|
93
|
-
<div className="flex gap-1 border-b border-slate-200 mb-4">
|
|
94
|
-
{(['active', 'invitations', 'former'] as ActiveTab[]).map((t) => (
|
|
95
|
-
(t === 'former' || t === 'invitations') && !canManage ? null : (
|
|
96
|
-
<button
|
|
97
|
-
key={t}
|
|
98
|
-
onClick={() => setTab(t)}
|
|
99
|
-
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors -mb-px ${
|
|
100
|
-
tab === t
|
|
101
|
-
? 'border-blue-500 text-blue-600'
|
|
102
|
-
: 'border-transparent text-slate-500 hover:text-slate-700'
|
|
103
|
-
}`}
|
|
104
|
-
>
|
|
105
|
-
{t === 'active' ? 'Active' : t === 'invitations' ? 'Pending Invitations' : 'Former Members'}
|
|
106
|
-
{t === 'active' && (
|
|
107
|
-
<span className="ml-1.5 inline-flex items-center px-1.5 py-0.5 rounded-full text-xs bg-slate-100 text-slate-600">
|
|
108
|
-
{activeMembers.length}
|
|
109
|
-
</span>
|
|
110
|
-
)}
|
|
111
|
-
{t === 'invitations' && (
|
|
112
|
-
<span className="ml-1.5 inline-flex items-center px-1.5 py-0.5 rounded-full text-xs bg-blue-100 text-blue-600">
|
|
113
|
-
{invitations.length}
|
|
114
|
-
</span>
|
|
115
|
-
)}
|
|
116
|
-
</button>
|
|
117
|
-
)
|
|
118
|
-
))}
|
|
119
|
-
</div>
|
|
120
|
-
|
|
121
|
-
{/* Active Members Tab */}
|
|
122
|
-
{tab === 'active' && (
|
|
123
|
-
<div>
|
|
124
|
-
{membersLoading && (
|
|
125
|
-
<div className="flex items-center justify-center py-12">
|
|
126
|
-
<div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
|
127
|
-
</div>
|
|
128
|
-
)}
|
|
129
|
-
{membersError && (
|
|
130
|
-
<div className="rounded-md bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">
|
|
131
|
-
{membersError}
|
|
132
|
-
</div>
|
|
133
|
-
)}
|
|
134
|
-
{!membersLoading && !membersError && (
|
|
135
|
-
<div className="overflow-hidden rounded-lg border border-slate-200 shadow-sm">
|
|
136
|
-
<table className="w-full text-left">
|
|
137
|
-
<thead className="bg-slate-50 border-b border-slate-200">
|
|
138
|
-
<tr>
|
|
139
|
-
<th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">Member</th>
|
|
140
|
-
<th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">Role</th>
|
|
141
|
-
<th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">Joined</th>
|
|
142
|
-
<th className="py-3 px-4" />
|
|
143
|
-
</tr>
|
|
144
|
-
</thead>
|
|
145
|
-
<tbody className="bg-white divide-y divide-slate-100">
|
|
146
|
-
{activeMembers.map((member) => (
|
|
147
|
-
<MemberRow
|
|
148
|
-
key={member.id}
|
|
149
|
-
member={member}
|
|
150
|
-
currentUserRole={currentRole}
|
|
151
|
-
onRemove={handleRemove}
|
|
152
|
-
onRoleChange={handleRoleChange}
|
|
153
|
-
/>
|
|
154
|
-
))}
|
|
155
|
-
{activeMembers.length === 0 && (
|
|
156
|
-
<tr>
|
|
157
|
-
<td colSpan={4} className="py-8 text-center text-sm text-slate-500">
|
|
158
|
-
No active members found.
|
|
159
|
-
</td>
|
|
160
|
-
</tr>
|
|
161
|
-
)}
|
|
162
|
-
</tbody>
|
|
163
|
-
</table>
|
|
164
|
-
</div>
|
|
165
|
-
)}
|
|
166
|
-
</div>
|
|
167
|
-
)}
|
|
168
|
-
|
|
169
|
-
{/* Pending Invitations Tab */}
|
|
170
|
-
{tab === 'invitations' && canManage && (
|
|
171
|
-
<div>
|
|
172
|
-
{invLoading && (
|
|
173
|
-
<div className="flex items-center justify-center py-12">
|
|
174
|
-
<div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
|
175
|
-
</div>
|
|
176
|
-
)}
|
|
177
|
-
{invError && (
|
|
178
|
-
<div className="rounded-md bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">
|
|
179
|
-
{invError}
|
|
180
|
-
</div>
|
|
181
|
-
)}
|
|
182
|
-
{!invLoading && !invError && (
|
|
183
|
-
<div className="overflow-hidden rounded-lg border border-slate-200 shadow-sm">
|
|
184
|
-
<table className="w-full text-left">
|
|
185
|
-
<thead className="bg-slate-50 border-b border-slate-200">
|
|
186
|
-
<tr>
|
|
187
|
-
<th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">Email</th>
|
|
188
|
-
<th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">Role</th>
|
|
189
|
-
<th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">Expires</th>
|
|
190
|
-
<th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">Actions</th>
|
|
191
|
-
</tr>
|
|
192
|
-
</thead>
|
|
193
|
-
<tbody className="bg-white divide-y divide-slate-100">
|
|
194
|
-
{invitations.map((inv) => (
|
|
195
|
-
<tr key={inv.id} className="hover:bg-slate-50 transition-colors">
|
|
196
|
-
<td className="py-3 px-4 text-sm text-slate-900">{inv.email}</td>
|
|
197
|
-
<td className="py-3 px-4"><RoleBadge role={inv.role} /></td>
|
|
198
|
-
<td className="py-3 px-4 text-xs text-slate-500">
|
|
199
|
-
{new Date(inv.expires_at).toLocaleDateString()}
|
|
200
|
-
</td>
|
|
201
|
-
<td className="py-3 px-4">
|
|
202
|
-
<div className="flex gap-3">
|
|
203
|
-
<button
|
|
204
|
-
onClick={() => handleResend(inv.id)}
|
|
205
|
-
className="text-xs text-blue-600 hover:text-blue-800 font-medium"
|
|
206
|
-
>
|
|
207
|
-
Resend
|
|
208
|
-
</button>
|
|
209
|
-
<button
|
|
210
|
-
onClick={() => handleRevoke(inv.id)}
|
|
211
|
-
className="text-xs text-red-600 hover:text-red-800 font-medium"
|
|
212
|
-
>
|
|
213
|
-
Revoke
|
|
214
|
-
</button>
|
|
215
|
-
</div>
|
|
216
|
-
</td>
|
|
217
|
-
</tr>
|
|
218
|
-
))}
|
|
219
|
-
{invitations.length === 0 && (
|
|
220
|
-
<tr>
|
|
221
|
-
<td colSpan={4} className="py-8 text-center text-sm text-slate-500">
|
|
222
|
-
No pending invitations.
|
|
223
|
-
</td>
|
|
224
|
-
</tr>
|
|
225
|
-
)}
|
|
226
|
-
</tbody>
|
|
227
|
-
</table>
|
|
228
|
-
</div>
|
|
229
|
-
)}
|
|
230
|
-
</div>
|
|
231
|
-
)}
|
|
232
|
-
|
|
233
|
-
{/* Former Members Tab */}
|
|
234
|
-
{tab === 'former' && canManage && (
|
|
235
|
-
<div>
|
|
236
|
-
{formerLoading && (
|
|
237
|
-
<div className="flex items-center justify-center py-12">
|
|
238
|
-
<div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
|
239
|
-
</div>
|
|
240
|
-
)}
|
|
241
|
-
{formerError && (
|
|
242
|
-
<div className="rounded-md bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">
|
|
243
|
-
{formerError}
|
|
244
|
-
</div>
|
|
245
|
-
)}
|
|
246
|
-
{!formerLoading && !formerError && (
|
|
247
|
-
<div className="overflow-hidden rounded-lg border border-slate-200 shadow-sm">
|
|
248
|
-
<table className="w-full text-left">
|
|
249
|
-
<thead className="bg-slate-50 border-b border-slate-200">
|
|
250
|
-
<tr>
|
|
251
|
-
<th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">Member</th>
|
|
252
|
-
<th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">Former Role</th>
|
|
253
|
-
<th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">Removed</th>
|
|
254
|
-
<th className="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-slate-500">Reason</th>
|
|
255
|
-
</tr>
|
|
256
|
-
</thead>
|
|
257
|
-
<tbody className="bg-white divide-y divide-slate-100">
|
|
258
|
-
{formerOnly.map((member) => (
|
|
259
|
-
<tr key={member.id} className="hover:bg-slate-50 transition-colors">
|
|
260
|
-
<td className="py-3 px-4">
|
|
261
|
-
<div className="flex flex-col">
|
|
262
|
-
<span className="text-sm font-medium text-slate-700">{member.name ?? member.email}</span>
|
|
263
|
-
{member.name && <span className="text-xs text-slate-400">{member.email}</span>}
|
|
264
|
-
</div>
|
|
265
|
-
</td>
|
|
266
|
-
<td className="py-3 px-4"><RoleBadge role={member.role} /></td>
|
|
267
|
-
<td className="py-3 px-4 text-xs text-slate-500">
|
|
268
|
-
{member.removed_at ? new Date(member.removed_at).toLocaleDateString() : '—'}
|
|
269
|
-
</td>
|
|
270
|
-
<td className="py-3 px-4 text-xs text-slate-500">
|
|
271
|
-
{member.removal_reason ?? '—'}
|
|
272
|
-
</td>
|
|
273
|
-
</tr>
|
|
274
|
-
))}
|
|
275
|
-
{formerOnly.length === 0 && (
|
|
276
|
-
<tr>
|
|
277
|
-
<td colSpan={4} className="py-8 text-center text-sm text-slate-500">
|
|
278
|
-
No former members.
|
|
279
|
-
</td>
|
|
280
|
-
</tr>
|
|
281
|
-
)}
|
|
282
|
-
</tbody>
|
|
283
|
-
</table>
|
|
284
|
-
</div>
|
|
285
|
-
)}
|
|
286
|
-
</div>
|
|
287
|
-
)}
|
|
288
|
-
</div>
|
|
289
|
-
);
|
|
290
|
-
}
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { getOrg, updateOrg, deleteOrg, initiateTransfer } from '../api.js';
|
|
3
|
-
import { DangerZoneCard } from '../components/DangerZoneCard.js';
|
|
4
|
-
import { useCurrentMembership } from '../hooks/useCurrentMembership.js';
|
|
5
|
-
import type { PublicOrg } from '../types.js';
|
|
6
|
-
|
|
7
|
-
interface OrgSettingsPageProps {
|
|
8
|
-
orgId: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function OrgSettingsPage({ orgId }: OrgSettingsPageProps) {
|
|
12
|
-
const { membership } = useCurrentMembership();
|
|
13
|
-
const [org, setOrg] = useState<PublicOrg | null>(null);
|
|
14
|
-
const [loading, setLoading] = useState(true);
|
|
15
|
-
const [error, setError] = useState<string | null>(null);
|
|
16
|
-
|
|
17
|
-
const [name, setName] = useState('');
|
|
18
|
-
const [slug, setSlug] = useState('');
|
|
19
|
-
const [saving, setSaving] = useState(false);
|
|
20
|
-
const [saveError, setSaveError] = useState<string | null>(null);
|
|
21
|
-
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
22
|
-
|
|
23
|
-
const [transferUserId, setTransferUserId] = useState('');
|
|
24
|
-
const [transferError, setTransferError] = useState<string | null>(null);
|
|
25
|
-
const [transferSuccess, setTransferSuccess] = useState(false);
|
|
26
|
-
|
|
27
|
-
const isOwner = membership?.role === 'owner';
|
|
28
|
-
const isAdmin = membership?.role === 'admin' || isOwner;
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
setLoading(true);
|
|
32
|
-
getOrg(orgId)
|
|
33
|
-
.then((o) => {
|
|
34
|
-
setOrg(o);
|
|
35
|
-
setName(o.name);
|
|
36
|
-
setSlug(o.slug);
|
|
37
|
-
})
|
|
38
|
-
.catch((e: Error) => setError(e.message))
|
|
39
|
-
.finally(() => setLoading(false));
|
|
40
|
-
}, [orgId]);
|
|
41
|
-
|
|
42
|
-
async function handleSave(e: React.FormEvent) {
|
|
43
|
-
e.preventDefault();
|
|
44
|
-
setSaving(true);
|
|
45
|
-
setSaveError(null);
|
|
46
|
-
setSaveSuccess(false);
|
|
47
|
-
try {
|
|
48
|
-
const updated = await updateOrg(orgId, { name, slug });
|
|
49
|
-
setOrg(updated);
|
|
50
|
-
setSaveSuccess(true);
|
|
51
|
-
setTimeout(() => setSaveSuccess(false), 3000);
|
|
52
|
-
} catch (err) {
|
|
53
|
-
setSaveError(err instanceof Error ? err.message : 'Failed to save');
|
|
54
|
-
} finally {
|
|
55
|
-
setSaving(false);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function handleDelete() {
|
|
60
|
-
await deleteOrg(orgId, org!.name);
|
|
61
|
-
window.location.href = '/';
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function handleInitiateTransfer() {
|
|
65
|
-
const uid = parseInt(transferUserId, 10);
|
|
66
|
-
if (!uid) { setTransferError('Enter a valid user ID'); return; }
|
|
67
|
-
setTransferError(null);
|
|
68
|
-
try {
|
|
69
|
-
await initiateTransfer(orgId, uid);
|
|
70
|
-
setTransferSuccess(true);
|
|
71
|
-
setTransferUserId('');
|
|
72
|
-
} catch (err) {
|
|
73
|
-
setTransferError(err instanceof Error ? err.message : 'Failed to initiate transfer');
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (loading) {
|
|
78
|
-
return (
|
|
79
|
-
<div className="flex items-center justify-center min-h-64">
|
|
80
|
-
<div className="h-8 w-8 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
|
81
|
-
</div>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (error) {
|
|
86
|
-
return (
|
|
87
|
-
<div className="max-w-2xl mx-auto px-4 py-8">
|
|
88
|
-
<div className="rounded-md bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">{error}</div>
|
|
89
|
-
</div>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<div className="max-w-2xl mx-auto px-4 py-8 space-y-8">
|
|
95
|
-
<h1 className="text-2xl font-bold text-slate-900">Organization Settings</h1>
|
|
96
|
-
|
|
97
|
-
{/* General Settings */}
|
|
98
|
-
{isAdmin && (
|
|
99
|
-
<section className="bg-white rounded-lg border border-slate-200 shadow-sm p-6">
|
|
100
|
-
<h2 className="text-lg font-semibold text-slate-800 mb-4">General</h2>
|
|
101
|
-
<form onSubmit={handleSave} className="space-y-4">
|
|
102
|
-
<div>
|
|
103
|
-
<label className="block text-sm font-medium text-slate-700 mb-1">
|
|
104
|
-
Organization Name
|
|
105
|
-
</label>
|
|
106
|
-
<input
|
|
107
|
-
type="text"
|
|
108
|
-
value={name}
|
|
109
|
-
onChange={(e) => setName(e.target.value)}
|
|
110
|
-
required
|
|
111
|
-
disabled={saving}
|
|
112
|
-
className="w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-slate-50"
|
|
113
|
-
/>
|
|
114
|
-
</div>
|
|
115
|
-
<div>
|
|
116
|
-
<label className="block text-sm font-medium text-slate-700 mb-1">
|
|
117
|
-
Slug
|
|
118
|
-
</label>
|
|
119
|
-
<input
|
|
120
|
-
type="text"
|
|
121
|
-
value={slug}
|
|
122
|
-
onChange={(e) => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-'))}
|
|
123
|
-
required
|
|
124
|
-
disabled={saving}
|
|
125
|
-
className="w-full rounded-md border border-slate-300 px-3 py-2 text-sm font-mono focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-slate-50"
|
|
126
|
-
/>
|
|
127
|
-
<p className="mt-1 text-xs text-slate-500">
|
|
128
|
-
Used in URLs. Lowercase letters, numbers, and hyphens only.
|
|
129
|
-
</p>
|
|
130
|
-
</div>
|
|
131
|
-
{saveError && <p className="text-sm text-red-600">{saveError}</p>}
|
|
132
|
-
{saveSuccess && <p className="text-sm text-green-600">Settings saved!</p>}
|
|
133
|
-
<button
|
|
134
|
-
type="submit"
|
|
135
|
-
disabled={saving}
|
|
136
|
-
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 disabled:opacity-50 transition-colors"
|
|
137
|
-
>
|
|
138
|
-
{saving ? 'Saving…' : 'Save Changes'}
|
|
139
|
-
</button>
|
|
140
|
-
</form>
|
|
141
|
-
</section>
|
|
142
|
-
)}
|
|
143
|
-
|
|
144
|
-
{/* Danger Zone — Owner only */}
|
|
145
|
-
{isOwner && (
|
|
146
|
-
<section className="space-y-4">
|
|
147
|
-
<h2 className="text-lg font-semibold text-slate-800">Danger Zone</h2>
|
|
148
|
-
|
|
149
|
-
{/* Ownership Transfer */}
|
|
150
|
-
<div className="border border-amber-200 rounded-lg p-5 bg-amber-50">
|
|
151
|
-
<h3 className="text-sm font-semibold text-amber-800 mb-1">Transfer Ownership</h3>
|
|
152
|
-
<p className="text-sm text-amber-700 mb-3">
|
|
153
|
-
Transfer ownership of this organization to another member. The recipient must accept within 72 hours.
|
|
154
|
-
</p>
|
|
155
|
-
<div className="flex gap-2">
|
|
156
|
-
<input
|
|
157
|
-
type="number"
|
|
158
|
-
placeholder="Target user ID"
|
|
159
|
-
value={transferUserId}
|
|
160
|
-
onChange={(e) => setTransferUserId(e.target.value)}
|
|
161
|
-
className="flex-1 rounded-md border border-amber-300 px-3 py-2 text-sm focus:border-amber-500 focus:outline-none focus:ring-1 focus:ring-amber-500"
|
|
162
|
-
/>
|
|
163
|
-
<button
|
|
164
|
-
onClick={handleInitiateTransfer}
|
|
165
|
-
className="rounded-md border border-amber-600 bg-white px-4 py-2 text-sm font-medium text-amber-700 hover:bg-amber-100 transition-colors"
|
|
166
|
-
>
|
|
167
|
-
Initiate Transfer
|
|
168
|
-
</button>
|
|
169
|
-
</div>
|
|
170
|
-
{transferError && <p className="mt-2 text-sm text-red-600">{transferError}</p>}
|
|
171
|
-
{transferSuccess && <p className="mt-2 text-sm text-green-600">Transfer initiated! Awaiting recipient acceptance.</p>}
|
|
172
|
-
</div>
|
|
173
|
-
|
|
174
|
-
<DangerZoneCard
|
|
175
|
-
title="Delete Organization"
|
|
176
|
-
description="Permanently delete this organization and all its data. This action cannot be undone."
|
|
177
|
-
buttonLabel="Delete Organization"
|
|
178
|
-
onConfirm={handleDelete}
|
|
179
|
-
confirmPrompt={org?.name}
|
|
180
|
-
/>
|
|
181
|
-
</section>
|
|
182
|
-
)}
|
|
183
|
-
</div>
|
|
184
|
-
);
|
|
185
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { usePendingTransfer } from '../hooks/usePendingTransfer.js';
|
|
3
|
-
import { useCurrentMembership } from '../hooks/useCurrentMembership.js';
|
|
4
|
-
import { acceptTransfer, cancelTransfer } from '../api.js';
|
|
5
|
-
|
|
6
|
-
interface OwnershipTransferPageProps {
|
|
7
|
-
orgId: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function OwnershipTransferPage({ orgId }: OwnershipTransferPageProps) {
|
|
11
|
-
const { transfer, loading, error } = usePendingTransfer(orgId);
|
|
12
|
-
const { membership } = useCurrentMembership();
|
|
13
|
-
const [confirmEmail, setConfirmEmail] = useState('');
|
|
14
|
-
const [submitting, setSubmitting] = useState(false);
|
|
15
|
-
const [actionError, setActionError] = useState<string | null>(null);
|
|
16
|
-
const [done, setDone] = useState<'accepted' | 'cancelled' | null>(null);
|
|
17
|
-
|
|
18
|
-
const userEmail = membership ? (membership as { email?: string }).email ?? '' : '';
|
|
19
|
-
const isTarget = transfer?.to_user_id === membership?.user_id;
|
|
20
|
-
const emailConfirmed = confirmEmail.trim().toLowerCase() === userEmail.toLowerCase();
|
|
21
|
-
|
|
22
|
-
async function handleAccept() {
|
|
23
|
-
if (!emailConfirmed) return;
|
|
24
|
-
setSubmitting(true);
|
|
25
|
-
setActionError(null);
|
|
26
|
-
try {
|
|
27
|
-
await acceptTransfer(orgId);
|
|
28
|
-
setDone('accepted');
|
|
29
|
-
} catch (err) {
|
|
30
|
-
setActionError(err instanceof Error ? err.message : 'Failed to accept transfer');
|
|
31
|
-
} finally {
|
|
32
|
-
setSubmitting(false);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function handleCancel() {
|
|
37
|
-
setSubmitting(true);
|
|
38
|
-
setActionError(null);
|
|
39
|
-
try {
|
|
40
|
-
await cancelTransfer(orgId);
|
|
41
|
-
setDone('cancelled');
|
|
42
|
-
} catch (err) {
|
|
43
|
-
setActionError(err instanceof Error ? err.message : 'Failed to cancel transfer');
|
|
44
|
-
} finally {
|
|
45
|
-
setSubmitting(false);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (loading) {
|
|
50
|
-
return (
|
|
51
|
-
<div className="flex items-center justify-center min-h-64">
|
|
52
|
-
<div className="h-8 w-8 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
|
53
|
-
</div>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (error) {
|
|
58
|
-
return (
|
|
59
|
-
<div className="max-w-lg mx-auto px-4 py-8">
|
|
60
|
-
<div className="rounded-md bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">{error}</div>
|
|
61
|
-
</div>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (done === 'accepted') {
|
|
66
|
-
return (
|
|
67
|
-
<div className="max-w-lg mx-auto px-4 py-8 text-center">
|
|
68
|
-
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-green-100">
|
|
69
|
-
<svg className="h-7 w-7 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
70
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
71
|
-
</svg>
|
|
72
|
-
</div>
|
|
73
|
-
<h1 className="text-xl font-bold text-slate-900 mb-2">Ownership transferred!</h1>
|
|
74
|
-
<p className="text-sm text-slate-600 mb-6">You are now the owner of this organization.</p>
|
|
75
|
-
<a href={`/orgs/${orgId}`} className="inline-block rounded-md bg-blue-600 px-6 py-2.5 text-sm font-medium text-white hover:bg-blue-700 transition-colors">
|
|
76
|
-
Go to Organization
|
|
77
|
-
</a>
|
|
78
|
-
</div>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (done === 'cancelled') {
|
|
83
|
-
return (
|
|
84
|
-
<div className="max-w-lg mx-auto px-4 py-8 text-center">
|
|
85
|
-
<h1 className="text-xl font-bold text-slate-900 mb-2">Transfer cancelled.</h1>
|
|
86
|
-
<a href={`/orgs/${orgId}`} className="inline-block rounded-md border border-slate-300 px-6 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors">
|
|
87
|
-
Back to Organization
|
|
88
|
-
</a>
|
|
89
|
-
</div>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (!transfer || !isTarget) {
|
|
94
|
-
return (
|
|
95
|
-
<div className="max-w-lg mx-auto px-4 py-8">
|
|
96
|
-
<div className="rounded-md bg-slate-50 border border-slate-200 px-4 py-3 text-sm text-slate-600">
|
|
97
|
-
No pending ownership transfer found for you.
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<div className="max-w-lg mx-auto px-4 py-8">
|
|
105
|
-
<h1 className="text-2xl font-bold text-slate-900 mb-2">Accept Ownership</h1>
|
|
106
|
-
<p className="text-sm text-slate-600 mb-6">
|
|
107
|
-
You are being offered ownership of this organization. Accepting grants you full owner privileges.
|
|
108
|
-
</p>
|
|
109
|
-
|
|
110
|
-
<div className="bg-white rounded-lg border border-slate-200 shadow-sm p-6 mb-6">
|
|
111
|
-
<h2 className="text-sm font-semibold text-slate-800 mb-3">Owner Powers You Will Receive</h2>
|
|
112
|
-
<ul className="space-y-2 text-sm text-slate-700">
|
|
113
|
-
{[
|
|
114
|
-
'Delete the organization',
|
|
115
|
-
'Appoint and demote admins',
|
|
116
|
-
'Transfer ownership to others',
|
|
117
|
-
'Override all member permissions',
|
|
118
|
-
].map((power) => (
|
|
119
|
-
<li key={power} className="flex items-start gap-2">
|
|
120
|
-
<svg className="mt-0.5 h-4 w-4 shrink-0 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
121
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
122
|
-
</svg>
|
|
123
|
-
{power}
|
|
124
|
-
</li>
|
|
125
|
-
))}
|
|
126
|
-
</ul>
|
|
127
|
-
</div>
|
|
128
|
-
|
|
129
|
-
<div className="mb-4">
|
|
130
|
-
<label className="block text-sm font-medium text-slate-700 mb-1">
|
|
131
|
-
Type your email address to confirm:
|
|
132
|
-
</label>
|
|
133
|
-
<input
|
|
134
|
-
type="email"
|
|
135
|
-
value={confirmEmail}
|
|
136
|
-
onChange={(e) => setConfirmEmail(e.target.value)}
|
|
137
|
-
placeholder={userEmail || 'your@email.com'}
|
|
138
|
-
className="w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
139
|
-
/>
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
{actionError && <p className="mb-3 text-sm text-red-600">{actionError}</p>}
|
|
143
|
-
|
|
144
|
-
<div className="flex gap-3">
|
|
145
|
-
<button
|
|
146
|
-
onClick={handleAccept}
|
|
147
|
-
disabled={!emailConfirmed || submitting}
|
|
148
|
-
className="flex-1 rounded-md bg-blue-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
149
|
-
>
|
|
150
|
-
{submitting ? 'Processing…' : 'Accept Ownership'}
|
|
151
|
-
</button>
|
|
152
|
-
<button
|
|
153
|
-
onClick={handleCancel}
|
|
154
|
-
disabled={submitting}
|
|
155
|
-
className="rounded-md border border-slate-300 px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 disabled:opacity-50 transition-colors"
|
|
156
|
-
>
|
|
157
|
-
Decline
|
|
158
|
-
</button>
|
|
159
|
-
</div>
|
|
160
|
-
</div>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|