Changed resuce to status to allow cancellation, enabled email notifications
This commit is contained in:
+37
-2
@@ -41,6 +41,7 @@ import {
|
||||
} from './repositories/birdRepository.js';
|
||||
import { createIntegrationTokenRecord, listIntegrationTokens, revokeIntegrationToken } from './repositories/integrationTokenRepository.js';
|
||||
import {
|
||||
cancelRescueVerificationRequest,
|
||||
claimWorkspaceInvites,
|
||||
createWorkspace,
|
||||
deleteWorkspaceMember,
|
||||
@@ -503,10 +504,17 @@ const sendRescueStatusNotification = async ({
|
||||
}: {
|
||||
workspace: WorkspaceRow;
|
||||
ownerEmail: string | null;
|
||||
event: 'created' | 'converted' | 'status_changed';
|
||||
event: 'created' | 'converted' | 'status_changed' | 'canceled';
|
||||
}) => {
|
||||
const statusLabel = workspace.rescue_verification_status.replace(/_/g, ' ');
|
||||
const eventLabel = event === 'created' ? 'created' : event === 'converted' ? 'converted to rescue' : 'status updated';
|
||||
const eventLabel =
|
||||
event === 'created'
|
||||
? 'created'
|
||||
: event === 'converted'
|
||||
? 'converted to rescue'
|
||||
: event === 'canceled'
|
||||
? 'canceled rescue request'
|
||||
: 'status updated';
|
||||
const subject = `FlockPal rescue status: ${workspace.name} ${eventLabel}`;
|
||||
const escapedWorkspaceName = escapeHtml(workspace.name);
|
||||
const escapedStatusLabel = escapeHtml(statusLabel);
|
||||
@@ -1213,6 +1221,33 @@ app.put('/api/workspace', requireAuth, requireWriteAccess, requireWorkspaceRole(
|
||||
}
|
||||
});
|
||||
|
||||
app.post(
|
||||
'/api/workspace/rescue-status/cancel',
|
||||
requireAuth,
|
||||
requireSessionAuth,
|
||||
requireWorkspaceRole(['owner', 'assistant']),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const workspace = await cancelRescueVerificationRequest(req.auth!.workspace.id);
|
||||
|
||||
if (!workspace) {
|
||||
res.status(409).json({ error: 'Only pending rescue status requests can be canceled.' });
|
||||
return;
|
||||
}
|
||||
|
||||
await sendRescueStatusNotification({
|
||||
workspace,
|
||||
ownerEmail: req.auth!.user.email,
|
||||
event: 'canceled',
|
||||
});
|
||||
|
||||
res.json({ workspace: normalizeWorkspace(workspace) });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.get('/api/workspace/members', requireAuth, async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const members = await listWorkspaceMembers(req.auth!.workspace.id);
|
||||
|
||||
@@ -314,7 +314,18 @@ export const listRescueWorkspacesForAdmin = async () => {
|
||||
export const updateRescueVerificationStatus = async (workspaceId: number, status: RescueVerificationStatus) => {
|
||||
const result = await db.query<WorkspaceRow>(
|
||||
`UPDATE workspaces
|
||||
SET rescue_verification_status = $2,
|
||||
SET workspace_type = CASE
|
||||
WHEN $2 = 'rejected' THEN 'standard'
|
||||
ELSE workspace_type
|
||||
END,
|
||||
billing_plan = CASE
|
||||
WHEN $2 = 'rejected' THEN 'household_basic'
|
||||
ELSE billing_plan
|
||||
END,
|
||||
rescue_verification_status = CASE
|
||||
WHEN $2 = 'rejected' THEN 'not_required'
|
||||
ELSE $2
|
||||
END,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
AND workspace_type = 'rescue'
|
||||
@@ -325,6 +336,23 @@ export const updateRescueVerificationStatus = async (workspaceId: number, status
|
||||
return result.rows[0] ?? null;
|
||||
};
|
||||
|
||||
export const cancelRescueVerificationRequest = async (workspaceId: number) => {
|
||||
const result = await db.query<WorkspaceRow>(
|
||||
`UPDATE workspaces
|
||||
SET workspace_type = 'standard',
|
||||
billing_plan = 'household_basic',
|
||||
rescue_verification_status = 'not_required',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
AND workspace_type = 'rescue'
|
||||
AND rescue_verification_status = 'pending'
|
||||
RETURNING id, name, workspace_type, billing_email, billing_plan, subscription_status, rescue_verification_status, created_at, updated_at`,
|
||||
[workspaceId],
|
||||
);
|
||||
|
||||
return result.rows[0] ?? null;
|
||||
};
|
||||
|
||||
export const getPlatformAdminSummary = async () => {
|
||||
const result = await db.query<{
|
||||
total_birds: number;
|
||||
|
||||
+70
-9
@@ -834,6 +834,7 @@ function App() {
|
||||
const [applyingPhotoCrop, setApplyingPhotoCrop] = useState(false);
|
||||
const [savingBird, setSavingBird] = useState(false);
|
||||
const [savingWorkspace, setSavingWorkspace] = useState(false);
|
||||
const [cancelingRescueRequest, setCancelingRescueRequest] = useState(false);
|
||||
const [savingWorkspaceMember, setSavingWorkspaceMember] = useState(false);
|
||||
const [creatingWorkspace, setCreatingWorkspace] = useState(false);
|
||||
const [creatingIntegrationToken, setCreatingIntegrationToken] = useState(false);
|
||||
@@ -1576,18 +1577,17 @@ function App() {
|
||||
throw new Error('Unable to update rescue verification status.');
|
||||
}
|
||||
|
||||
setAdminRescueWorkspaces((current) =>
|
||||
current.map((entry) => (entry.workspace.id === workspaceId ? { ...entry, workspace: data.workspace! } : entry)),
|
||||
);
|
||||
const nextRescueWorkspaces = adminRescueWorkspaces
|
||||
.map((entry) => (entry.workspace.id === workspaceId ? { ...entry, workspace: data.workspace! } : entry))
|
||||
.filter((entry) => entry.workspace.workspaceType === 'rescue');
|
||||
|
||||
setAdminRescueWorkspaces(nextRescueWorkspaces);
|
||||
setAdminSummary((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
pendingRescues: adminRescueWorkspaces.filter((entry) =>
|
||||
entry.workspace.id === workspaceId
|
||||
? rescueVerificationStatus === 'pending'
|
||||
: entry.workspace.rescueVerificationStatus === 'pending',
|
||||
).length,
|
||||
rescueWorkspaces: nextRescueWorkspaces.length,
|
||||
pendingRescues: nextRescueWorkspaces.filter((entry) => entry.workspace.rescueVerificationStatus === 'pending').length,
|
||||
}
|
||||
: current,
|
||||
);
|
||||
@@ -2213,6 +2213,56 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelRescueRequest = async () => {
|
||||
if (!authToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
setCancelingRescueRequest(true);
|
||||
|
||||
try {
|
||||
const response = await apiFetch('/workspace/rescue-status/cancel', authToken, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await readErrorMessage(response, 'Unable to cancel rescue status request.'));
|
||||
}
|
||||
|
||||
const data = (await readJsonSafely<{ workspace?: Workspace }>(response)) ?? {};
|
||||
|
||||
if (!data.workspace) {
|
||||
throw new Error('Unable to cancel rescue status request.');
|
||||
}
|
||||
|
||||
const savedWorkspace = data.workspace;
|
||||
|
||||
setWorkspace(savedWorkspace);
|
||||
setAuthSession((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
activeWorkspace: savedWorkspace,
|
||||
workspaces: current.workspaces.map((entry) =>
|
||||
entry.workspace.id === savedWorkspace.id ? { ...entry, workspace: savedWorkspace } : entry,
|
||||
),
|
||||
}
|
||||
: current,
|
||||
);
|
||||
setWorkspaceForm({
|
||||
name: savedWorkspace.name,
|
||||
workspaceType: savedWorkspace.workspaceType,
|
||||
billingEmail: savedWorkspace.billingEmail ?? '',
|
||||
billingPlan: isHouseholdPlan(savedWorkspace.billingPlan) ? savedWorkspace.billingPlan : 'household_basic',
|
||||
});
|
||||
} catch (workspaceError) {
|
||||
setError(workspaceError instanceof Error ? workspaceError.message : 'Unable to cancel rescue status request.');
|
||||
} finally {
|
||||
setCancelingRescueRequest(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWorkspaceMemberSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setError('');
|
||||
@@ -2660,7 +2710,7 @@ function App() {
|
||||
type="button"
|
||||
disabled={updatingRescueWorkspaceId === entry.workspace.id || entry.workspace.rescueVerificationStatus === 'rejected'}
|
||||
>
|
||||
Reject
|
||||
Reject and make household
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
@@ -3195,6 +3245,17 @@ function App() {
|
||||
<article className="summary-card">
|
||||
<strong>{formatRescueVerificationStatus(workspace.rescueVerificationStatus)}</strong>
|
||||
<span>Rescue flocks are read-only until an admin approves their verification.</span>
|
||||
{workspace.rescueVerificationStatus === 'pending' &&
|
||||
(activeMembership?.role === 'owner' || activeMembership?.role === 'assistant') ? (
|
||||
<button
|
||||
className="secondary-button"
|
||||
type="button"
|
||||
onClick={handleCancelRescueRequest}
|
||||
disabled={cancelingRescueRequest}
|
||||
>
|
||||
{cancelingRescueRequest ? 'Canceling request...' : 'Cancel rescue request'}
|
||||
</button>
|
||||
) : null}
|
||||
</article>
|
||||
) : null}
|
||||
<article className="summary-card">
|
||||
|
||||
Reference in New Issue
Block a user