From 7526e5c2e5c7e96b109a864d708cb56fe633568c Mon Sep 17 00:00:00 2001 From: seaznCode Date: Fri, 20 Feb 2026 21:45:54 +0100 Subject: [PATCH] feat: enhance user detail modal with confirmation for document moves, add subscriptions navigation, and improve invoice status handling - Added ConfirmActionModal to UserDetailModal for document move confirmation. - Introduced My Subscriptions button in Header for easier navigation. - Enhanced FinanceInvoices component to display normalized invoice statuses with badges. - Created editAbo hook for managing subscription content updates. - Updated getAbo hook to include more subscription statuses and improved mapping logic. - Refactored ProfilePage to link to subscriptions page and removed unused state. - Implemented ProfileSubscriptionsPage for managing subscriptions with detailed views and actions. - Replaced custom modal in DeactivateReferralLinkModal with ConfirmActionModal for consistency. --- .../components/contractEditor.tsx | 38 +- .../components/contractTemplateList.tsx | 51 +- src/app/admin/pool-management/manage/page.tsx | 22 +- src/app/admin/pool-management/page.tsx | 52 +- .../hooks/getActiveCoffees.ts | 94 ++-- src/app/components/UserDetailModal.tsx | 33 +- src/app/components/nav/Header.tsx | 9 + .../profile/components/financeInvoices.tsx | 40 +- src/app/profile/hooks/editAbo.ts | 56 ++ src/app/profile/hooks/getAbo.ts | 108 +++- src/app/profile/page.tsx | 23 +- src/app/profile/subscriptions/page.tsx | 492 ++++++++++++++++++ .../deactivateReferralLinkModal.tsx | 94 +--- 13 files changed, 920 insertions(+), 192 deletions(-) create mode 100644 src/app/profile/hooks/editAbo.ts create mode 100644 src/app/profile/subscriptions/page.tsx diff --git a/src/app/admin/contract-management/components/contractEditor.tsx b/src/app/admin/contract-management/components/contractEditor.tsx index 6fd3be2..24b2483 100644 --- a/src/app/admin/contract-management/components/contractEditor.tsx +++ b/src/app/admin/contract-management/components/contractEditor.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import useContractManagement from '../hooks/useContractManagement'; +import ConfirmActionModal from '../../../components/modals/ConfirmActionModal'; type Props = { editingTemplateId?: string | null; @@ -23,6 +24,8 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi const [description, setDescription] = useState(''); const [editingMeta, setEditingMeta] = useState<{ id: string; version: number; state: string } | null>(null); + const [publishConfirmOpen, setPublishConfirmOpen] = useState(false) + const [publishConfirmMessage, setPublishConfirmMessage] = useState('') const iframeRef = useRef(null); @@ -152,7 +155,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi lang ) - const save = async (publish: boolean) => { + const doSave = async (publish: boolean) => { const html = htmlCode.trim(); // NEW: validate all fields if (!canSave) { @@ -160,14 +163,6 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi return; } - if (publish && type === 'contract') { - const kind = contractType === 'gdpr' ? 'GDPR' : 'Contract'; - const ok = window.confirm( - `Activate this ${kind} template now?\n\nThis will deactivate other active ${kind} templates that apply to the same user type and language.` - ); - if (!ok) return; - } - setSaving(true); setStatusMsg(null); @@ -218,6 +213,21 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi } }; + const save = async (publish: boolean) => { + if (publish && type === 'contract') { + const kind = contractType === 'gdpr' ? 'GDPR' : 'Contract'; + setPublishConfirmMessage(`This will deactivate other active ${kind} templates that apply to the same user type and language.`) + setPublishConfirmOpen(true) + return + } + await doSave(publish) + } + + const confirmPublish = async () => { + setPublishConfirmOpen(false) + await doSave(true) + } + return (
{editingMeta && ( @@ -372,6 +382,16 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi {saving && Saving…} {statusMsg && {statusMsg}}
+ + !saving && setPublishConfirmOpen(false)} + onConfirm={confirmPublish} + /> ); } diff --git a/src/app/admin/contract-management/components/contractTemplateList.tsx b/src/app/admin/contract-management/components/contractTemplateList.tsx index 6142837..045fec3 100644 --- a/src/app/admin/contract-management/components/contractTemplateList.tsx +++ b/src/app/admin/contract-management/components/contractTemplateList.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import useContractManagement from '../hooks/useContractManagement'; +import ConfirmActionModal from '../../../components/modals/ConfirmActionModal'; type Props = { refreshKey?: number; @@ -37,6 +38,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props) const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [q, setQ] = useState(''); + const [pendingToggle, setPendingToggle] = useState<{ id: string; target: 'active' | 'inactive'; message?: string; requiresConfirm: boolean } | null>(null) const { listTemplates, @@ -83,29 +85,39 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props) // eslint-disable-next-line react-hooks/exhaustive-deps }, [refreshKey]); - const onToggleState = async (id: string, current: string) => { - const target = current === 'published' ? 'inactive' : 'active'; - - // Confirmation: activating a contract/GDPR will deactivate other active templates of the same kind - if (target === 'active') { - const tpl = items.find((i) => i.id === id); - if (tpl?.type === 'contract') { - const kind = tpl.contract_type === 'gdpr' ? 'GDPR' : 'Contract'; - const ok = window.confirm( - `Activate this ${kind} template now?\n\nThis will deactivate other active ${kind} templates that apply to the same user type and language.` - ); - if (!ok) return; - } - } - + const executeToggleState = async (id: string, target: 'active' | 'inactive') => { try { const updated = await updateTemplateState(id, target as 'active' | 'inactive'); // Update clicked item immediately, then refresh list to reflect any auto-deactivations. setItems((prev) => prev.map((i) => i.id === id ? { ...i, status: updated.state === 'active' ? 'published' : 'draft' } : i)); await load(); } catch {} + } + + const onToggleState = async (id: string, current: string) => { + const target = current === 'published' ? 'inactive' : 'active'; + if (target === 'active') { + const tpl = items.find((i) => i.id === id); + if (tpl?.type === 'contract') { + const kind = tpl.contract_type === 'gdpr' ? 'GDPR' : 'Contract'; + setPendingToggle({ + id, + target, + requiresConfirm: true, + message: `This will deactivate other active ${kind} templates that apply to the same user type and language.`, + }); + return; + } + } + await executeToggleState(id, target); }; + const confirmToggleState = async () => { + if (!pendingToggle) return + await executeToggleState(pendingToggle.id, pendingToggle.target) + setPendingToggle(null) + } + const onPreview = (id: string) => openPreviewInNewTab(id); const onGenPdf = async (id: string) => { @@ -191,6 +203,15 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
No contracts found.
)} + + setPendingToggle(null)} + onConfirm={confirmToggleState} + /> ); } diff --git a/src/app/admin/pool-management/manage/page.tsx b/src/app/admin/pool-management/manage/page.tsx index 8b2acab..6fd61ec 100644 --- a/src/app/admin/pool-management/manage/page.tsx +++ b/src/app/admin/pool-management/manage/page.tsx @@ -9,6 +9,7 @@ import useAuthStore from '../../../store/authStore' import PageTransitionEffect from '../../../components/animation/pageTransitionEffect' import { AdminAPI } from '../../../utils/api' import { authFetch } from '../../../utils/authFetch' +import ConfirmActionModal from '../../../components/modals/ConfirmActionModal' type PoolUser = { id: string @@ -78,6 +79,7 @@ function PoolManagePageInner() { const [savingMembers, setSavingMembers] = React.useState(false) const [removingMemberId, setRemovingMemberId] = React.useState(null) const [removeError, setRemoveError] = React.useState('') + const [removeConfirm, setRemoveConfirm] = React.useState<{ userId: string; label: string } | null>(null) const [subscriptions, setSubscriptions] = React.useState>([]) const [linkedSubscriptionId, setLinkedSubscriptionId] = React.useState(initialSubscriptionId) const [savingSubscription, setSavingSubscription] = React.useState(false) @@ -293,10 +295,14 @@ function PoolManagePageInner() { } async function removeMember(userId: string) { - if (!token || !poolId || poolId === 'pool-unknown') return const user = users.find(u => u.id === userId) const label = user?.name || user?.email || 'this user' - if (!window.confirm(`Remove ${label} from this pool?`)) return + setRemoveConfirm({ userId, label }) + } + + async function confirmRemoveMember() { + if (!token || !poolId || poolId === 'pool-unknown' || !removeConfirm) return + const userId = removeConfirm.userId setRemoveError('') setRemovingMemberId(userId) try { @@ -306,6 +312,7 @@ function PoolManagePageInner() { setRemoveError(e?.message || 'Failed to remove user from pool.') } finally { setRemovingMemberId(null) + setRemoveConfirm(null) } } @@ -638,6 +645,17 @@ function PoolManagePageInner() { )} + + { if (!removingMemberId) setRemoveConfirm(null) }} + onConfirm={confirmRemoveMember} + /> ) diff --git a/src/app/admin/pool-management/page.tsx b/src/app/admin/pool-management/page.tsx index 6aa88c1..78e0796 100644 --- a/src/app/admin/pool-management/page.tsx +++ b/src/app/admin/pool-management/page.tsx @@ -12,6 +12,7 @@ import { setPoolInactive, setPoolActive } from './hooks/poolStatus' import PageTransitionEffect from '../../components/animation/pageTransitionEffect' import CreateNewPoolModal from './components/createNewPoolModal' import { authFetch } from '../../utils/authFetch' +import ConfirmActionModal from '../../components/modals/ConfirmActionModal' type Pool = { id: string @@ -35,6 +36,8 @@ export default function PoolManagementPage() { const [createSuccess, setCreateSuccess] = React.useState('') const [createModalOpen, setCreateModalOpen] = React.useState(false) const [archiveError, setArchiveError] = React.useState('') + const [poolStatusConfirm, setPoolStatusConfirm] = React.useState<{ poolId: string; action: 'archive' | 'activate' } | null>(null) + const [poolStatusPending, setPoolStatusPending] = React.useState(false) // Token and API URL const token = useAuthStore(s => s.accessToken) @@ -128,26 +131,28 @@ export default function PoolManagementPage() { } async function handleArchive(poolId: string) { - const confirmed = window.confirm('Archive this pool? Users will no longer be able to join or use it.') - if (!confirmed) return - setArchiveError('') - const res = await setPoolInactive(poolId) - if (res.ok) { - await refresh?.() - } else { - setArchiveError(res.message || 'Failed to deactivate pool.') - } + setPoolStatusConfirm({ poolId, action: 'archive' }) } async function handleSetActive(poolId: string) { - const confirmed = window.confirm('Unarchive this pool and make it active again?') - if (!confirmed) return + setPoolStatusConfirm({ poolId, action: 'activate' }) + } + + async function confirmPoolStatusChange() { + if (!poolStatusConfirm) return + const { poolId, action } = poolStatusConfirm + setPoolStatusPending(true) setArchiveError('') - const res = await setPoolActive(poolId) - if (res.ok) { - await refresh?.() - } else { - setArchiveError(res.message || 'Failed to activate pool.') + try { + const res = action === 'archive' ? await setPoolInactive(poolId) : await setPoolActive(poolId) + if (res.ok) { + await refresh?.() + } else { + setArchiveError(res.message || (action === 'archive' ? 'Failed to deactivate pool.' : 'Failed to activate pool.')) + } + } finally { + setPoolStatusPending(false) + setPoolStatusConfirm(null) } } @@ -344,6 +349,21 @@ export default function PoolManagementPage() { clearMessages={() => { setCreateError(''); setCreateSuccess(''); }} /> + { if (!poolStatusPending) setPoolStatusConfirm(null) }} + onConfirm={confirmPoolStatusChange} + /> +