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
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import { useEffect, useState } from "react"
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"
import { Button } from "~/components/ui/button"

import { TableProps } from "./table"
import { type Payment, type Project, type User } from "~/server/db/types"

export default function ExportButton({ data }: TableProps) {
export interface ExportButtonProps {
data: Array<Project | Payment | User>
label: string
}

export default function ExportButton({ data, label }: ExportButtonProps) {
const [open, setOpen] = useState(false)
useEffect(() => {
if (!open) return
Expand All @@ -17,19 +22,44 @@ export default function ExportButton({ data }: TableProps) {
const downloadCSV = () => {
if (!data || data.length === 0) {
setOpen(true)
console.log("open")
return
}

const headers = Object.keys(data[0] as Record<string, unknown>).join(",")
const rows = data.map((row) => Object.values(row).join(",")).join("\n")
const csv = headers + "\n" + rows
const headers = Object.keys(data[0] as Record<string, unknown>)
const escapeCSV = (value: unknown): string => {
if (value == null) return ""

let str: string
if (typeof value === "object") {
if (value instanceof Date) {
str = value.toISOString()
} else {
// Stringify JSON objects/arrays
str = JSON.stringify(value)
}
} else {
str = String(value)
}

// Escape CSV: wrap in quotes if contains comma, quote, or newline
// and escape internal quotes by doubling them
if (/[",\n\r]/.test(str)) {
return `"${str.replace(/"/g, '""')}"`
}
return str
}

const rows = data
.map((row) => headers.map((key) => escapeCSV((row as Record<string, unknown>)[key])).join(","))
.join("\n")

const csv = headers.join(",") + "\n" + rows
const blob = new Blob([csv], { type: "text/csv" })
const url = window.URL.createObjectURL(blob)

const a = document.createElement("a")
a.setAttribute("href", url)
a.setAttribute("download", "output.csv")
a.setAttribute("download", `${label}.csv`)
a.click()
window.URL.revokeObjectURL(url)
setOpen(true)
Expand All @@ -38,7 +68,7 @@ export default function ExportButton({ data }: TableProps) {
return (
<div className="relative inline-block">
<Button onClick={downloadCSV} variant={"outline"}>
Export CSV
Export {label}
</Button>
{open && (!data || data.length === 0) && (
<Alert variant="destructive" className="fixed top-30 left-1/2 -translate-x-1/2 z-50 max-w-sm">
Expand All @@ -49,7 +79,7 @@ export default function ExportButton({ data }: TableProps) {
{open && data && data.length > 0 && (
<Alert variant="default" className="fixed top-30 left-1/2 -translate-x-1/2 z-50 max-w-sm">
<AlertTitle>Export Successful</AlertTitle>
<AlertDescription>Your data has been exported as `output.csv`.</AlertDescription>
<AlertDescription>Your data has been exported as {`${label}.csv`}.</AlertDescription>
</Alert>
)}
</div>
Expand Down
10 changes: 10 additions & 0 deletions src/app/dashboard/admin/@tools/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { api } from "~/trpc/server"

import ExportButton from "./export"
import UpdateEmail from "./update-email"

export default async function AdminUserTable() {
const projects = await api.admin.projects.getAllProjects.query()
const payments = await api.admin.analytics.getAllPayments.query()

return (
<>
<div className="flex h-[50px] items-center p-1">
Expand All @@ -9,6 +15,10 @@ export default async function AdminUserTable() {
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<UpdateEmail />
</div>
<div className=" flex flex-col space-y-4 my-4">
<ExportButton data={projects} label="projects" />
<ExportButton data={payments} label="payments" />
</div>
</>
)
}
12 changes: 6 additions & 6 deletions src/app/dashboard/admin/@users/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@ import { cn } from "~/lib/utils"
import { type User } from "~/server/db/types"
import { api } from "~/trpc/react"

import ExportButton from "./export"
import ExportButton from "../@tools/export"
import AddUserForm from "./form"

type UserProps = Omit<User, "subscribe" | "square_customer_id" | "updatedAt" | "reminder_pending">
type DisplayColumn = Omit<User, "subscribe" | "square_customer_id" | "updatedAt" | "reminder_pending">

export interface TableProps {
data: Array<UserProps>
data: Array<User>
}
interface UserTableProps extends TableProps {
refetch: () => void
Expand All @@ -92,7 +92,7 @@ const sortIcon = (sortOrder: string | boolean) => {
}
}

const columns = (updateRole: ({ id, role }: UpdateUserRoleFunctionProps) => void): ColumnDef<UserProps>[] => [
const columns = (updateRole: ({ id, role }: UpdateUserRoleFunctionProps) => void): ColumnDef<DisplayColumn>[] => [
{
id: "Select",
enableSorting: false,
Expand Down Expand Up @@ -341,7 +341,7 @@ const UserTable = ({ data, isRefetching, ...props }: UserTableProps) => {

return (
<>
<div className="flex h-[50px] items-center gap-2 p-1 pr-0">
<div className="flex h-12 items-center gap-2 p-1 pr-0">
{data.length > 0 && (
<>
{selectedRowIDs.length > 0 && (
Expand Down Expand Up @@ -418,7 +418,7 @@ const UserTable = ({ data, isRefetching, ...props }: UserTableProps) => {
})}
</DropdownMenuContent>
</DropdownMenu>
<ExportButton data={data} />
<ExportButton data={data} label="users" />
<Button variant="secondary" disabled={isRefetching} onClick={props.refetch}>
<span className={cn("material-symbols-sharp", isRefetching && "animate-spin")}>autorenew</span>
<span className="ml-2 hidden sm:block">Sync{isRefetching && "ing"}</span>
Expand Down
13 changes: 12 additions & 1 deletion src/app/profile/[id]/profile.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client"

import { format } from "date-fns"
import Link from "next/link"
import { siDiscord, siGithub } from "simple-icons"

Expand Down Expand Up @@ -115,7 +116,7 @@ const ProfilePage = ({ id, currentUser }: ProfilePageProps) => {
<div className="p-2 w-auto">
<div className="">
{currentUser?.id === user.id ? (
user?.role === null && (
user?.role === null ? (
<div className="space-y-4 max-w-md">
<div className="space-y-2">
<h2 className="font-semibold leading-none tracking-tight">Membership</h2>
Expand All @@ -140,6 +141,16 @@ const ProfilePage = ({ id, currentUser }: ProfilePageProps) => {
</p>
)}
</div>
) : (
user.role === "member" &&
user.membership_expiry && (
<div>
Your membership will expire on{" "}
<span className="text-primary">
{format(new Date(String(user.membership_expiry)), "dd MMMM yyyy")}
</span>
</div>
)
)
) : (
<>
Expand Down
12 changes: 12 additions & 0 deletions src/server/api/routers/admin/analytics/get-payments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { desc } from "drizzle-orm"

import { adminProcedure } from "~/server/api/trpc"
import { Payment } from "~/server/db/schema"

export const getAllPayments = adminProcedure.query(async ({ ctx }) => {
const paymentList = await ctx.db.query.Payment.findMany({
orderBy: [desc(Payment.createdAt), desc(Payment.id)],
})

return paymentList
})
2 changes: 2 additions & 0 deletions src/server/api/routers/admin/analytics/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { createTRPCRouter } from "~/server/api/trpc"

import { getGenderStatistics } from "./get-gender-statistics"
import { getAllPayments } from "./get-payments"
import { getUserCount } from "./get-user-count"
import { getUsersPerDay } from "./get-users-per-day"

export const analyticsAdminRouter = createTRPCRouter({
getUserCount,
getUsersPerDay,
getGenderStatistics,
getAllPayments,
})
12 changes: 12 additions & 0 deletions src/server/api/routers/admin/projects/get-all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { desc } from "drizzle-orm"

import { adminProcedure } from "~/server/api/trpc"
import { Project } from "~/server/db/schema"

export const getAllProjects = adminProcedure.query(async ({ ctx }) => {
const projectList = await ctx.db.query.Project.findMany({
orderBy: [desc(Project.createdAt), desc(Project.id)],
})

return projectList
})
2 changes: 2 additions & 0 deletions src/server/api/routers/admin/projects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createTRPCRouter } from "~/server/api/trpc"

import { create } from "./create"
import { deleteProject } from "./delete"
import { getAllProjects } from "./get-all"
import { getProjectById } from "./get-project-by-id"
import { getProjects, getPublicProjects } from "./get-projects"
import { update } from "./update"
Expand All @@ -13,4 +14,5 @@ export const projectsAdminRouter = createTRPCRouter({
update,
getProjectById,
deleteProject,
getAllProjects,
})
6 changes: 0 additions & 6 deletions src/server/api/routers/admin/users/get-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ import { User } from "~/server/db/schema"

export const getAll = adminProcedure.query(async ({ ctx }) => {
const userList = await ctx.db.query.User.findMany({
columns: {
subscribe: false,
square_customer_id: false,
updatedAt: false,
reminder_pending: false,
},
orderBy: [desc(User.createdAt), desc(User.id)],
})

Expand Down