changed portrait shape
This commit is contained in:
@@ -2992,6 +2992,12 @@ app.get('/api/admin/summary', requireAuth, requireSessionAuth, requireAdmin, asy
|
|||||||
rescueBirds: Number(summary?.rescue_birds ?? 0),
|
rescueBirds: Number(summary?.rescue_birds ?? 0),
|
||||||
pendingRescues: Number(summary?.pending_rescues ?? 0),
|
pendingRescues: Number(summary?.pending_rescues ?? 0),
|
||||||
dailyUsers: Number(summary?.daily_users ?? 0),
|
dailyUsers: Number(summary?.daily_users ?? 0),
|
||||||
|
subscriptionsByPlan: {
|
||||||
|
household_basic: Number(summary?.household_basic_subscriptions ?? 0),
|
||||||
|
household_plus: Number(summary?.household_plus_subscriptions ?? 0),
|
||||||
|
household_macaw: Number(summary?.household_macaw_subscriptions ?? 0),
|
||||||
|
household_hyacinth_macaw: Number(summary?.household_hyacinth_macaw_subscriptions ?? 0),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -531,6 +531,10 @@ test('getPlatformAdminSummary counts memorialized birds separately', async () =>
|
|||||||
rescue_birds: 5,
|
rescue_birds: 5,
|
||||||
pending_rescues: 1,
|
pending_rescues: 1,
|
||||||
daily_users: 2,
|
daily_users: 2,
|
||||||
|
household_basic_subscriptions: 2,
|
||||||
|
household_plus_subscriptions: 1,
|
||||||
|
household_macaw_subscriptions: 0,
|
||||||
|
household_hyacinth_macaw_subscriptions: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -543,4 +547,7 @@ test('getPlatformAdminSummary counts memorialized birds separately', async () =>
|
|||||||
assert.match(calls[0].text, /memorialized_at IS NOT NULL/);
|
assert.match(calls[0].text, /memorialized_at IS NOT NULL/);
|
||||||
assert.match(calls[0].text, /rescue_birds/);
|
assert.match(calls[0].text, /rescue_birds/);
|
||||||
assert.match(calls[0].text, /workspaces\.workspace_type = 'rescue'/);
|
assert.match(calls[0].text, /workspaces\.workspace_type = 'rescue'/);
|
||||||
|
assert.equal(summary?.household_basic_subscriptions, 2);
|
||||||
|
assert.match(calls[0].text, /billing_plan = 'household_basic'/);
|
||||||
|
assert.match(calls[0].text, /subscription_status IN \('active', 'trialing'\)/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -605,6 +605,10 @@ export const getPlatformAdminSummary = async () => {
|
|||||||
rescue_birds: number;
|
rescue_birds: number;
|
||||||
pending_rescues: number;
|
pending_rescues: number;
|
||||||
daily_users: number;
|
daily_users: number;
|
||||||
|
household_basic_subscriptions: number;
|
||||||
|
household_plus_subscriptions: number;
|
||||||
|
household_macaw_subscriptions: number;
|
||||||
|
household_hyacinth_macaw_subscriptions: number;
|
||||||
}>(
|
}>(
|
||||||
`SELECT
|
`SELECT
|
||||||
(SELECT COUNT(*)::int FROM birds) AS total_birds,
|
(SELECT COUNT(*)::int FROM birds) AS total_birds,
|
||||||
@@ -614,7 +618,11 @@ export const getPlatformAdminSummary = async () => {
|
|||||||
(SELECT COUNT(*)::int FROM workspaces WHERE workspace_type = 'rescue') AS rescue_workspaces,
|
(SELECT COUNT(*)::int FROM workspaces WHERE workspace_type = 'rescue') AS rescue_workspaces,
|
||||||
(SELECT COUNT(*)::int FROM birds INNER JOIN workspaces ON workspaces.id = birds.workspace_id WHERE workspaces.workspace_type = 'rescue') AS rescue_birds,
|
(SELECT COUNT(*)::int FROM birds INNER JOIN workspaces ON workspaces.id = birds.workspace_id WHERE workspaces.workspace_type = 'rescue') AS rescue_birds,
|
||||||
(SELECT COUNT(*)::int FROM workspaces WHERE workspace_type = 'rescue' AND rescue_verification_status = 'pending') AS pending_rescues,
|
(SELECT COUNT(*)::int FROM workspaces WHERE workspace_type = 'rescue' AND rescue_verification_status = 'pending') AS pending_rescues,
|
||||||
(SELECT COUNT(DISTINCT user_id)::int FROM auth_sessions WHERE created_at >= CURRENT_DATE) AS daily_users`,
|
(SELECT COUNT(DISTINCT user_id)::int FROM auth_sessions WHERE created_at >= CURRENT_DATE) AS daily_users,
|
||||||
|
(SELECT COUNT(*)::int FROM workspaces WHERE billing_plan = 'household_basic' AND subscription_status IN ('active', 'trialing')) AS household_basic_subscriptions,
|
||||||
|
(SELECT COUNT(*)::int FROM workspaces WHERE billing_plan = 'household_plus' AND subscription_status IN ('active', 'trialing')) AS household_plus_subscriptions,
|
||||||
|
(SELECT COUNT(*)::int FROM workspaces WHERE billing_plan = 'household_macaw' AND subscription_status IN ('active', 'trialing')) AS household_macaw_subscriptions,
|
||||||
|
(SELECT COUNT(*)::int FROM workspaces WHERE billing_plan = 'household_hyacinth_macaw' AND subscription_status IN ('active', 'trialing')) AS household_hyacinth_macaw_subscriptions`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
|
|||||||
+27
-20
@@ -184,6 +184,7 @@ type AdminSummary = {
|
|||||||
rescueBirds: number;
|
rescueBirds: number;
|
||||||
pendingRescues: number;
|
pendingRescues: number;
|
||||||
dailyUsers: number;
|
dailyUsers: number;
|
||||||
|
subscriptionsByPlan: Record<HouseholdBillingPlan, number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AdminRescueWorkspace = {
|
type AdminRescueWorkspace = {
|
||||||
@@ -1540,7 +1541,7 @@ const OVERVIEW_WINDOW_DAYS = 30;
|
|||||||
const OVERVIEW_HISTORY_DAYS = 425;
|
const OVERVIEW_HISTORY_DAYS = 425;
|
||||||
const OVERVIEW_WIDTH = 520;
|
const OVERVIEW_WIDTH = 520;
|
||||||
const OVERVIEW_HEIGHT = 220;
|
const OVERVIEW_HEIGHT = 220;
|
||||||
const OVERVIEW_PADDING = { top: 20, right: 18, bottom: 36, left: 52 };
|
const OVERVIEW_PADDING = { top: 48, right: 18, bottom: 36, left: 52 };
|
||||||
const PHOTO_MAX_BYTES = 900_000;
|
const PHOTO_MAX_BYTES = 900_000;
|
||||||
const PHOTO_EXPORT_SIZES = [720, 600, 480];
|
const PHOTO_EXPORT_SIZES = [720, 600, 480];
|
||||||
const PHOTO_EXPORT_QUALITIES = [0.9, 0.82, 0.74, 0.66];
|
const PHOTO_EXPORT_QUALITIES = [0.9, 0.82, 0.74, 0.66];
|
||||||
@@ -6341,42 +6342,42 @@ function App() {
|
|||||||
))}
|
))}
|
||||||
{points.length > 0 ? (() => {
|
{points.length > 0 ? (() => {
|
||||||
const latestPoint = points[points.length - 1];
|
const latestPoint = points[points.length - 1];
|
||||||
const portraitSize = 24;
|
const pointerRadius = 13;
|
||||||
const portraitGap = 8;
|
const pointerCenterY = latestPoint.y - 21;
|
||||||
const portraitX = latestPoint.x + portraitGap + portraitSize <= OVERVIEW_WIDTH - 2
|
const pointerTop = pointerCenterY - pointerRadius;
|
||||||
? latestPoint.x + portraitGap
|
const pointerPath = [
|
||||||
: latestPoint.x - portraitGap - portraitSize;
|
`M ${latestPoint.x} ${latestPoint.y}`,
|
||||||
const portraitY = Math.min(
|
`C ${latestPoint.x - 2.5} ${latestPoint.y - 5}, ${latestPoint.x - pointerRadius} ${latestPoint.y - 13}, ${latestPoint.x - pointerRadius} ${pointerCenterY}`,
|
||||||
Math.max(latestPoint.y - portraitSize / 2, 2),
|
`A ${pointerRadius} ${pointerRadius} 0 1 1 ${latestPoint.x + pointerRadius} ${pointerCenterY}`,
|
||||||
OVERVIEW_HEIGHT - OVERVIEW_PADDING.bottom - portraitSize - 2,
|
`C ${latestPoint.x + pointerRadius} ${latestPoint.y - 13}, ${latestPoint.x + 2.5} ${latestPoint.y - 5}, ${latestPoint.x} ${latestPoint.y}`,
|
||||||
);
|
'Z',
|
||||||
|
].join(' ');
|
||||||
const clipPathId = `overview-bird-portrait-${bird.id}`;
|
const clipPathId = `overview-bird-portrait-${bird.id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g className="overview-latest-portrait">
|
<g className="overview-latest-portrait">
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id={clipPathId}>
|
<clipPath id={clipPathId}>
|
||||||
<circle cx={portraitX + portraitSize / 2} cy={portraitY + portraitSize / 2} r={portraitSize / 2} />
|
<path d={pointerPath} />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
<image
|
<image
|
||||||
href={bird.photoDataUrl || defaultBirdPhoto}
|
href={bird.photoDataUrl || defaultBirdPhoto}
|
||||||
x={portraitX}
|
x={latestPoint.x - pointerRadius}
|
||||||
y={portraitY}
|
y={pointerTop}
|
||||||
width={portraitSize}
|
width={pointerRadius * 2}
|
||||||
height={portraitSize}
|
height={latestPoint.y - pointerTop}
|
||||||
preserveAspectRatio="xMidYMid slice"
|
preserveAspectRatio="xMidYMid slice"
|
||||||
clipPath={`url(#${clipPathId})`}
|
clipPath={`url(#${clipPathId})`}
|
||||||
>
|
>
|
||||||
<title>{`${bird.name}'s latest weight: ${latestPoint.label}`}</title>
|
<title>{`${bird.name}'s latest weight: ${latestPoint.label}`}</title>
|
||||||
</image>
|
</image>
|
||||||
<circle
|
<path
|
||||||
cx={portraitX + portraitSize / 2}
|
d={pointerPath}
|
||||||
cy={portraitY + portraitSize / 2}
|
|
||||||
r={portraitSize / 2}
|
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={bird.chartColor}
|
stroke={bird.chartColor}
|
||||||
strokeWidth="2.5"
|
strokeWidth="2"
|
||||||
|
strokeLinejoin="round"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
@@ -6589,6 +6590,12 @@ function App() {
|
|||||||
<span>Pending rescues</span>
|
<span>Pending rescues</span>
|
||||||
<strong>{adminSummary?.pendingRescues ?? '-'}</strong>
|
<strong>{adminSummary?.pendingRescues ?? '-'}</strong>
|
||||||
</article>
|
</article>
|
||||||
|
{(['household_basic', 'household_plus', 'household_macaw', 'household_hyacinth_macaw'] as const).map((billingPlan) => (
|
||||||
|
<article className="summary-card" key={billingPlan}>
|
||||||
|
<span>{formatBillingPlanName(billingPlan)} subscriptions</span>
|
||||||
|
<strong>{adminSummary?.subscriptionsByPlan?.[billingPlan] ?? '-'}</strong>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user