@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.
Files changed (254) hide show
  1. package/dist/client/actions.d.ts +20 -0
  2. package/dist/client/actions.d.ts.map +1 -0
  3. package/dist/client/actions.js +23 -0
  4. package/dist/client/actions.js.map +1 -0
  5. package/dist/client/api.d.ts +74 -0
  6. package/dist/client/api.d.ts.map +1 -0
  7. package/dist/client/api.js +245 -0
  8. package/dist/client/api.js.map +1 -0
  9. package/dist/client/components/AddMemberForm.d.ts +12 -0
  10. package/dist/client/components/AddMemberForm.d.ts.map +1 -0
  11. package/dist/client/components/AddMemberForm.js +37 -0
  12. package/dist/client/components/AddMemberForm.js.map +1 -0
  13. package/dist/client/components/AuditEventRow.d.ts +7 -0
  14. package/dist/client/components/AuditEventRow.d.ts.map +1 -0
  15. package/dist/client/components/AuditEventRow.js +20 -0
  16. package/dist/client/components/AuditEventRow.js.map +1 -0
  17. package/dist/client/components/CascadePreview.d.ts +11 -0
  18. package/dist/client/components/CascadePreview.d.ts.map +1 -0
  19. package/dist/client/components/CascadePreview.js +7 -0
  20. package/dist/client/components/CascadePreview.js.map +1 -0
  21. package/dist/client/components/DangerZoneCard.d.ts +10 -0
  22. package/dist/client/components/DangerZoneCard.d.ts.map +1 -0
  23. package/dist/client/components/DangerZoneCard.js +28 -0
  24. package/dist/client/components/DangerZoneCard.js.map +1 -0
  25. package/dist/client/components/InvitationCodeDisplay.d.ts +7 -0
  26. package/dist/client/components/InvitationCodeDisplay.d.ts.map +1 -0
  27. package/dist/client/components/InvitationCodeDisplay.js +26 -0
  28. package/dist/client/components/InvitationCodeDisplay.js.map +1 -0
  29. package/dist/client/components/InviteForm.d.ts +7 -0
  30. package/dist/client/components/InviteForm.d.ts.map +1 -0
  31. package/dist/client/components/InviteForm.js +35 -0
  32. package/dist/client/components/InviteForm.js.map +1 -0
  33. package/dist/client/components/MemberRow.d.ts +10 -0
  34. package/dist/client/components/MemberRow.d.ts.map +1 -0
  35. package/dist/client/components/MemberRow.js +17 -0
  36. package/dist/client/components/MemberRow.js.map +1 -0
  37. package/dist/client/components/OrgPeopleRoster.d.ts +11 -0
  38. package/dist/client/components/OrgPeopleRoster.d.ts.map +1 -0
  39. package/dist/client/components/OrgPeopleRoster.js +13 -0
  40. package/dist/client/components/OrgPeopleRoster.js.map +1 -0
  41. package/dist/client/components/PendingTransferBanner.d.ts +10 -0
  42. package/dist/client/components/PendingTransferBanner.d.ts.map +1 -0
  43. package/dist/client/components/PendingTransferBanner.js +48 -0
  44. package/dist/client/components/PendingTransferBanner.js.map +1 -0
  45. package/dist/client/components/PlaceholderCard.d.ts +7 -0
  46. package/dist/client/components/PlaceholderCard.d.ts.map +1 -0
  47. package/dist/client/components/PlaceholderCard.js +15 -0
  48. package/dist/client/components/PlaceholderCard.js.map +1 -0
  49. package/dist/client/components/RoleBadge.d.ts +5 -0
  50. package/dist/client/components/RoleBadge.d.ts.map +1 -0
  51. package/dist/client/components/RoleBadge.js +17 -0
  52. package/dist/client/components/RoleBadge.js.map +1 -0
  53. package/dist/client/components/RoleSelect.d.ts +10 -0
  54. package/dist/client/components/RoleSelect.d.ts.map +1 -0
  55. package/dist/client/components/RoleSelect.js +12 -0
  56. package/dist/client/components/RoleSelect.js.map +1 -0
  57. package/dist/client/components/SeatUsagePanel.d.ts +6 -0
  58. package/dist/client/components/SeatUsagePanel.d.ts.map +1 -0
  59. package/dist/client/components/SeatUsagePanel.js +13 -0
  60. package/dist/client/components/SeatUsagePanel.js.map +1 -0
  61. package/dist/client/hooks/useCurrentMembership.d.ts +8 -0
  62. package/dist/client/hooks/useCurrentMembership.d.ts.map +1 -0
  63. package/dist/client/hooks/useCurrentMembership.js +20 -0
  64. package/dist/client/hooks/useCurrentMembership.js.map +1 -0
  65. package/dist/client/hooks/useMembers.d.ts +10 -0
  66. package/dist/client/hooks/useMembers.d.ts.map +1 -0
  67. package/dist/client/hooks/useMembers.js +20 -0
  68. package/dist/client/hooks/useMembers.js.map +1 -0
  69. package/dist/client/hooks/useOrgMembers.d.ts +20 -0
  70. package/dist/client/hooks/useOrgMembers.d.ts.map +1 -0
  71. package/dist/client/hooks/useOrgMembers.js +63 -0
  72. package/dist/client/hooks/useOrgMembers.js.map +1 -0
  73. package/dist/client/hooks/usePendingInvitations.d.ts +8 -0
  74. package/dist/client/hooks/usePendingInvitations.d.ts.map +1 -0
  75. package/dist/client/hooks/usePendingInvitations.js +20 -0
  76. package/dist/client/hooks/usePendingInvitations.js.map +1 -0
  77. package/dist/client/hooks/usePendingTransfer.d.ts +8 -0
  78. package/dist/client/hooks/usePendingTransfer.d.ts.map +1 -0
  79. package/dist/client/hooks/usePendingTransfer.js +23 -0
  80. package/dist/client/hooks/usePendingTransfer.js.map +1 -0
  81. package/dist/client/index.d.ts +31 -0
  82. package/dist/client/index.d.ts.map +1 -0
  83. package/{src/client/index.ts → dist/client/index.js} +6 -54
  84. package/dist/client/index.js.map +1 -0
  85. package/dist/client/pages/AuditLogPage.d.ts +6 -0
  86. package/dist/client/pages/AuditLogPage.d.ts.map +1 -0
  87. package/dist/client/pages/AuditLogPage.js +51 -0
  88. package/dist/client/pages/AuditLogPage.js.map +1 -0
  89. package/dist/client/pages/EmailChangePage.d.ts +8 -0
  90. package/dist/client/pages/EmailChangePage.d.ts.map +1 -0
  91. package/dist/client/pages/EmailChangePage.js +52 -0
  92. package/dist/client/pages/EmailChangePage.js.map +1 -0
  93. package/dist/client/pages/InvitationAcceptPage.d.ts +11 -0
  94. package/dist/client/pages/InvitationAcceptPage.d.ts.map +1 -0
  95. package/dist/client/pages/InvitationAcceptPage.js +42 -0
  96. package/dist/client/pages/InvitationAcceptPage.js.map +1 -0
  97. package/dist/client/pages/InvitationCodePage.d.ts +6 -0
  98. package/dist/client/pages/InvitationCodePage.d.ts.map +1 -0
  99. package/dist/client/pages/InvitationCodePage.js +28 -0
  100. package/dist/client/pages/InvitationCodePage.js.map +1 -0
  101. package/dist/client/pages/MembersPage.d.ts +6 -0
  102. package/dist/client/pages/MembersPage.d.ts.map +1 -0
  103. package/dist/client/pages/MembersPage.js +67 -0
  104. package/dist/client/pages/MembersPage.js.map +1 -0
  105. package/dist/client/pages/OrgPeoplePage.d.ts +14 -0
  106. package/dist/client/pages/OrgPeoplePage.d.ts.map +1 -0
  107. package/dist/client/pages/OrgPeoplePage.js +40 -0
  108. package/dist/client/pages/OrgPeoplePage.js.map +1 -0
  109. package/dist/client/pages/OrgSettingsPage.d.ts +6 -0
  110. package/dist/client/pages/OrgSettingsPage.d.ts.map +1 -0
  111. package/dist/client/pages/OrgSettingsPage.js +78 -0
  112. package/dist/client/pages/OrgSettingsPage.js.map +1 -0
  113. package/dist/client/pages/OwnershipTransferPage.d.ts +6 -0
  114. package/dist/client/pages/OwnershipTransferPage.d.ts.map +1 -0
  115. package/dist/client/pages/OwnershipTransferPage.js +68 -0
  116. package/dist/client/pages/OwnershipTransferPage.js.map +1 -0
  117. package/dist/client/pages/PasswordResetPage.d.ts +6 -0
  118. package/dist/client/pages/PasswordResetPage.d.ts.map +1 -0
  119. package/dist/client/pages/PasswordResetPage.js +34 -0
  120. package/dist/client/pages/PasswordResetPage.js.map +1 -0
  121. package/dist/client/pages/PasswordResetRequestPage.d.ts +2 -0
  122. package/dist/client/pages/PasswordResetRequestPage.d.ts.map +1 -0
  123. package/dist/client/pages/PasswordResetRequestPage.js +27 -0
  124. package/dist/client/pages/PasswordResetRequestPage.js.map +1 -0
  125. package/dist/client/pages/PlaceholderPage.d.ts +7 -0
  126. package/dist/client/pages/PlaceholderPage.d.ts.map +1 -0
  127. package/dist/client/pages/PlaceholderPage.js +16 -0
  128. package/dist/client/pages/PlaceholderPage.js.map +1 -0
  129. package/dist/client/pages/SuperAdminDashboard.d.ts +2 -0
  130. package/dist/client/pages/SuperAdminDashboard.d.ts.map +1 -0
  131. package/dist/client/pages/SuperAdminDashboard.js +123 -0
  132. package/dist/client/pages/SuperAdminDashboard.js.map +1 -0
  133. package/dist/client/theme.d.ts +12 -0
  134. package/dist/client/theme.d.ts.map +1 -0
  135. package/dist/client/theme.js +16 -0
  136. package/dist/client/theme.js.map +1 -0
  137. package/dist/client/types.d.ts +78 -0
  138. package/dist/client/types.d.ts.map +1 -0
  139. package/dist/client/types.js +2 -0
  140. package/dist/client/types.js.map +1 -0
  141. package/dist/index.d.ts +3 -1
  142. package/dist/index.d.ts.map +1 -1
  143. package/dist/index.js +2 -1
  144. package/dist/index.js.map +1 -1
  145. package/dist/server/index.d.ts +2 -0
  146. package/dist/server/index.d.ts.map +1 -1
  147. package/dist/server/index.js +1 -0
  148. package/dist/server/index.js.map +1 -1
  149. package/dist/server/org-admin.d.ts +45 -0
  150. package/dist/server/org-admin.d.ts.map +1 -0
  151. package/dist/server/org-admin.js +63 -0
  152. package/dist/server/org-admin.js.map +1 -0
  153. package/dist/server/routes/orgs.routes.d.ts.map +1 -1
  154. package/dist/server/routes/orgs.routes.js +81 -12
  155. package/dist/server/routes/orgs.routes.js.map +1 -1
  156. package/dist/server/types.d.ts +2 -0
  157. package/dist/server/types.d.ts.map +1 -1
  158. package/dist/server/types.js.map +1 -1
  159. package/package.json +18 -11
  160. package/.eslintrc.cjs +0 -18
  161. package/CHANGELOG.md +0 -159
  162. package/src/client/api.ts +0 -314
  163. package/src/client/components/AuditEventRow.tsx +0 -59
  164. package/src/client/components/CascadePreview.tsx +0 -36
  165. package/src/client/components/DangerZoneCard.tsx +0 -103
  166. package/src/client/components/InvitationCodeDisplay.tsx +0 -48
  167. package/src/client/components/InviteForm.tsx +0 -77
  168. package/src/client/components/MemberRow.tsx +0 -69
  169. package/src/client/components/PendingTransferBanner.tsx +0 -98
  170. package/src/client/components/PlaceholderCard.tsx +0 -26
  171. package/src/client/components/RoleBadge.tsx +0 -26
  172. package/src/client/components/RoleSelect.tsx +0 -35
  173. package/src/client/hooks/.gitkeep +0 -0
  174. package/src/client/hooks/useCurrentMembership.ts +0 -24
  175. package/src/client/hooks/useMembers.ts +0 -24
  176. package/src/client/hooks/usePendingInvitations.ts +0 -24
  177. package/src/client/hooks/usePendingTransfer.ts +0 -27
  178. package/src/client/pages/AuditLogPage.tsx +0 -164
  179. package/src/client/pages/EmailChangePage.tsx +0 -144
  180. package/src/client/pages/InvitationAcceptPage.tsx +0 -163
  181. package/src/client/pages/InvitationCodePage.tsx +0 -108
  182. package/src/client/pages/MembersPage.tsx +0 -290
  183. package/src/client/pages/OrgSettingsPage.tsx +0 -185
  184. package/src/client/pages/OwnershipTransferPage.tsx +0 -163
  185. package/src/client/pages/PasswordResetPage.tsx +0 -104
  186. package/src/client/pages/PasswordResetRequestPage.tsx +0 -71
  187. package/src/client/pages/PlaceholderPage.tsx +0 -20
  188. package/src/client/pages/SuperAdminDashboard.tsx +0 -401
  189. package/src/client/types.ts +0 -78
  190. package/src/index.ts +0 -24
  191. package/src/server/crypto.ts +0 -47
  192. package/src/server/index.ts +0 -167
  193. package/src/server/middleware/require-membership.ts +0 -48
  194. package/src/server/middleware/require-role.ts +0 -19
  195. package/src/server/middleware/require-super-admin.ts +0 -32
  196. package/src/server/migrations/0001_create_tm_schema_migrations.sql +0 -13
  197. package/src/server/migrations/0002_create_tm_organizations.sql +0 -14
  198. package/src/server/migrations/0003_create_tm_memberships.sql +0 -24
  199. package/src/server/migrations/0004_create_tm_invitations.sql +0 -22
  200. package/src/server/migrations/0005_create_tm_audit_events.sql +0 -17
  201. package/src/server/migrations/0006_create_tm_email_change_requests.sql +0 -13
  202. package/src/server/migrations/0007_create_tm_ownership_transfers.sql +0 -22
  203. package/src/server/migrations/0008_create_tm_super_admins.sql +0 -8
  204. package/src/server/migrations/0009_create_tm_password_reset_requests.sql +0 -9
  205. package/src/server/migrations/0010_create_tm_shared_access.sql +0 -8
  206. package/src/server/migrations/0011_seed_super_admin.sql +0 -15
  207. package/src/server/migrations/0012_create_tm_user_locks.sql +0 -7
  208. package/src/server/routes/admin.routes.ts +0 -208
  209. package/src/server/routes/audit.routes.ts +0 -93
  210. package/src/server/routes/health.routes.ts +0 -46
  211. package/src/server/routes/invitations.routes.ts +0 -252
  212. package/src/server/routes/me.routes.ts +0 -143
  213. package/src/server/routes/orgs.routes.ts +0 -428
  214. package/src/server/routes/transfer.routes.ts +0 -110
  215. package/src/server/services/.gitkeep +0 -0
  216. package/src/server/services/audit.service.ts +0 -49
  217. package/src/server/services/email-change.service.ts +0 -178
  218. package/src/server/services/invitations.service.ts +0 -316
  219. package/src/server/services/memberships.service.ts +0 -129
  220. package/src/server/services/organizations.service.ts +0 -110
  221. package/src/server/services/ownership.service.ts +0 -170
  222. package/src/server/services/password-reset.service.ts +0 -94
  223. package/src/server/services/super-admin.service.ts +0 -321
  224. package/src/server/sql/.gitkeep +0 -0
  225. package/src/server/types.ts +0 -145
  226. package/src/shared/types.ts +0 -24
  227. package/tests/integration/audit-fires.test.ts +0 -288
  228. package/tests/integration/cascade-preview.test.ts +0 -157
  229. package/tests/integration/email-change.test.ts +0 -190
  230. package/tests/integration/feature-flags.test.ts +0 -213
  231. package/tests/integration/invitations-code.test.ts +0 -218
  232. package/tests/integration/invitations-expiry.test.ts +0 -216
  233. package/tests/integration/invitations-resend.test.ts +0 -241
  234. package/tests/integration/invitations-revoke.test.ts +0 -226
  235. package/tests/integration/invitations-switch-org.test.ts +0 -156
  236. package/tests/integration/invitations-token.test.ts +0 -221
  237. package/tests/integration/migrations.test.ts +0 -119
  238. package/tests/integration/only-owner-protections.test.ts +0 -130
  239. package/tests/integration/org-lifecycle.test.ts +0 -169
  240. package/tests/integration/ownership-transfer-cancel.test.ts +0 -171
  241. package/tests/integration/ownership-transfer-expire.test.ts +0 -171
  242. package/tests/integration/ownership-transfer-happy.test.ts +0 -184
  243. package/tests/integration/ownership-transfer-locks.test.ts +0 -146
  244. package/tests/integration/password-reset.test.ts +0 -200
  245. package/tests/integration/super-admin-actions.test.ts +0 -180
  246. package/tests/integration/super-admin-restrictions.test.ts +0 -209
  247. package/tests/setup/global-setup.ts +0 -20
  248. package/tests/unit/adapter-shape.test.ts +0 -330
  249. package/tests/unit/role-permissions.test.ts +0 -236
  250. package/tests/unit/validation.test.ts +0 -304
  251. package/tsconfig.client.json +0 -13
  252. package/tsconfig.json +0 -12
  253. package/tsconfig.tsbuildinfo +0 -1
  254. 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
-