Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@

# Amplify build output
/.amplify-hosting/

# Local notes
docs/next-up.md
114 changes: 114 additions & 0 deletions app/api/resource-edits/methods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { SupabaseClient } from "@supabase/supabase-js";
import type {
EditReviewStatus,
ResourceEdit,
ResourceEditQueueRow,
} from "~/types/ResourceEdit";
import type { ResourceEntry } from "~/types/ResourceEntry";

const TABLE_NAME = "resource_edits";

export const getResourceEditAPI = (client: SupabaseClient) => {
const table = client.from(TABLE_NAME);

return {
/** All pending edits to existing resources, joined to the current resource for compare. */
getEditsQueue: async () => {
const { data, error } = await client
.from("resource_edits_queue")
.select("*")
.order("submitted_at", { ascending: false });

if (error) {
throw error;
}

return (data ?? []) as ResourceEditQueueRow[];
},
/** All pending brand-new site submissions (no mapped_resource). */
getNewResourcesQueue: async () => {
const { data, error } = await client
.from("new_resources_queue")
.select("*")
.order("submitted_at", { ascending: false });

if (error) {
throw error;
}

return (data ?? []) as ResourceEdit[];
},
getList: async (params: { review_status?: EditReviewStatus } = {}) => {
let query = table.select("*").order("submitted_at", { ascending: false });

if (params.review_status) {
query = query.eq("review_status", params.review_status);
}

const { data, error } = await query;

if (error) {
throw error;
}

return (data ?? []) as ResourceEdit[];
},
getById: async (id: number) => {
const { data, error } = await table
.select("*")
.eq("id", id)
.single<ResourceEdit>();

if (error) {
throw error;
}

return data;
},
/** Partial update to the proposed values while an edit is PENDING. */
updateFields: async (id: number, values: Partial<ResourceEntry>) => {
const { data, error } = await table
.update(values)
.eq("id", id)
.select()
.single<ResourceEdit>();

if (error) {
throw error;
}

return data;
},
/** Approves the edit via the `approve_edit` RPC — applies it to `resources`
* (updating the mapped resource, or inserting a new one) so the change
* goes live on the map. Returns the resulting `resources` row. */
approve: async (id: number, reviewer: string) => {
const { data, error } = await client
.rpc("approve_edit", { p_edit_id: id, p_reviewer: reviewer })
.single<ResourceEntry & { id: number }>();

if (error) {
throw error;
}

return data;
},
reject: async (id: number, reviewer: string, notes?: string) => {
const { data, error } = await client
.rpc("reject_edit", {
p_edit_id: id,
p_reviewer: reviewer,
p_notes: notes ?? null,
})
.single<ResourceEdit>();

if (error) {
throw error;
}

return data;
},
};
};

export default getResourceEditAPI;
69 changes: 0 additions & 69 deletions app/api/resource-revisions/methods.ts

This file was deleted.

74 changes: 50 additions & 24 deletions app/routes/authenticated/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ import {
TableCell,
TableContainer,
TableRow,
Tooltip,
Typography,
} from "@mui/material";
import type { LoaderFunction } from "react-router";
import { Link, useLoaderData } from "react-router";
import { getDatabaseClient } from "~/api/client.server";
import { getResourceRevisionAPI } from "~/api/resource-revisions/methods";
import { getResourceEditAPI } from "~/api/resource-edits/methods";
import { authMiddleware } from "~/middleware/auth";
import type { ResourceType } from "~/types/ResourceEntry";
import {
Expand All @@ -29,22 +28,30 @@ const RESOURCE_TYPES: ResourceType[] = ["WATER", "FOOD", "FORAGE", "BATHROOM"];

export const loader: LoaderFunction = async ({ request }) => {
const { client } = getDatabaseClient(request);
const revisionAPI = getResourceRevisionAPI(client);
const revisions = await revisionAPI.getList();
const editAPI = getResourceEditAPI(client);
const edits = await editAPI.getList();

const submitterCounts = new Map<string, number>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we run aggregation queries on the database level instead of manipulating the data in code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will fix in a future PR, have an issue to track this

const approverCounts = new Map<string, number>();
const outstandingByType = new Map<string, number>();
let pendingCount = 0;

for (const revision of revisions) {
const creator = revision.creator || "Unknown";
for (const edit of edits) {
const creator = edit.creator || "Unknown";
submitterCounts.set(creator, (submitterCounts.get(creator) ?? 0) + 1);

if (revision.review_status === "PENDING") {
if (edit.review_status === "PENDING") {
pendingCount += 1;
outstandingByType.set(
revision.resource_type,
(outstandingByType.get(revision.resource_type) ?? 0) + 1,
edit.resource_type,
(outstandingByType.get(edit.resource_type) ?? 0) + 1,
);
}

if (edit.review_status === "APPROVED" && edit.reviewed_by) {
approverCounts.set(
edit.reviewed_by,
(approverCounts.get(edit.reviewed_by) ?? 0) + 1,
);
}
}
Expand All @@ -54,23 +61,30 @@ export const loader: LoaderFunction = async ({ request }) => {
.slice(0, 5)
.map(([creator, count]) => ({ creator, count }));

const topApprovers = [...approverCounts.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([reviewer, count]) => ({ reviewer, count }));

const outstanding = RESOURCE_TYPES.map((type) => ({
type,
count: outstandingByType.get(type) ?? 0,
}));

return {
totalRevisions: revisions.length,
totalEdits: edits.length,
pendingCount,
topSubmitters,
topApprovers,
outstanding,
};
};

type LoaderData = {
totalRevisions: number;
totalEdits: number;
pendingCount: number;
topSubmitters: { creator: string; count: number }[];
topApprovers: { reviewer: string; count: number }[];
outstanding: { type: string; count: number }[];
};

Expand Down Expand Up @@ -121,7 +135,7 @@ const StatCard = ({
);

const Dashboard = () => {
const { totalRevisions, pendingCount, topSubmitters, outstanding } =
const { totalEdits, pendingCount, topSubmitters, topApprovers, outstanding } =
useLoaderData<LoaderData>();

return (
Expand All @@ -143,7 +157,7 @@ const Dashboard = () => {
/>
<StatCard
label="Total submissions"
value={totalRevisions}
value={totalEdits}
icon={<HourglassTop fontSize="small" />}
/>
</Stack>
Expand Down Expand Up @@ -214,18 +228,30 @@ const Dashboard = () => {
)}
</Paper>

<Paper
variant="outlined"
sx={{ p: 2.5, flex: 1, bgcolor: "action.hover" }}
>
<Tooltip title="resource_edits has no reviewed_by column yet, so there's no record of who approved/rejected a given submission. Add that column (and set it in the approve/reject action) to unlock this.">
<Typography variant="subtitle1" fontWeight={600} gutterBottom>
Top approvers
</Typography>
</Tooltip>
<Typography variant="body2" color="text.secondary">
Not trackable yet — approvals don't record who approved them.
<Paper variant="outlined" sx={{ p: 2.5, flex: 1 }}>
<Typography variant="subtitle1" fontWeight={600} gutterBottom>
Top approvers
</Typography>
{topApprovers.length === 0 ? (
<Typography variant="body2" color="text.secondary">
No approvals yet.
</Typography>
) : (
<TableContainer>
<Table size="small">
<TableBody>
{topApprovers.map(({ reviewer, count }) => (
<TableRow key={reviewer}>
<TableCell>{reviewer}</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>
{count}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Paper>
</Stack>
</Stack>
Expand Down
Loading