Skip to content

Commit

Permalink
Customers wip
Browse files Browse the repository at this point in the history
  • Loading branch information
pontusab committed Dec 5, 2024
1 parent 25bce89 commit 5856ce1
Show file tree
Hide file tree
Showing 12 changed files with 682 additions and 3 deletions.
44 changes: 44 additions & 0 deletions apps/dashboard/src/app/[locale]/(app)/(sidebar)/customers/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { CustomersHeader } from "@/components/customers-header";
import { ErrorFallback } from "@/components/error-fallback";
import { CustomersTable } from "@/components/tables/customers";
import { CustomersSkeleton } from "@/components/tables/customers/skeleton";
import type { Metadata } from "next";
import { ErrorBoundary } from "next/dist/client/components/error-boundary";
import { Suspense } from "react";
import { searchParamsCache } from "./search-params";

export const metadata: Metadata = {
title: "Customers | Midday",
};

export default async function Page({
searchParams,
}: {
searchParams: Record<string, string | string[] | undefined>;
}) {
const {
q: query,
sort,
start,
end,
page,
} = searchParamsCache.parse(searchParams);

return (
<div className="flex flex-col pt-6 gap-6">
<CustomersHeader />

<ErrorBoundary errorComponent={ErrorFallback}>
<Suspense fallback={<CustomersSkeleton />}>
<CustomersTable
query={query}
sort={sort}
start={start}
end={end}
page={page}
/>
</Suspense>
</ErrorBoundary>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {
createSearchParamsCache,
parseAsArrayOf,
parseAsInteger,
parseAsString,
} from "nuqs/server";

export const searchParamsCache = createSearchParamsCache({
page: parseAsInteger.withDefault(0),
q: parseAsString.withDefault(""),
sort: parseAsArrayOf(parseAsString),
start: parseAsString,
end: parseAsString,
});
15 changes: 15 additions & 0 deletions apps/dashboard/src/components/customers-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { InvoiceSearchFilter } from "@/components/invoice-search-filter";
import { getCustomers } from "@midday/supabase/cached-queries";
import { OpenInvoiceSheet } from "./open-invoice-sheet";

export async function CustomersHeader() {
return (
<div className="flex items-center justify-between">
<InvoiceSearchFilter />

<div className="hidden sm:block">
<OpenInvoiceSheet />
</div>
</div>
);
}
7 changes: 6 additions & 1 deletion apps/dashboard/src/components/main-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const icons = {
"/transactions": () => <Icons.Transactions size={22} />,
"/invoices": () => <Icons.Invoice size={22} />,
"/tracker": () => <Icons.Tracker size={22} />,
"/vault": () => <Icons.Files size={22} />,
"/customers": () => <Icons.Customers size={22} />,
"/vault": () => <Icons.Vault size={22} />,
"/settings": () => <Icons.Settings size={22} />,
"/apps": () => <Icons.Apps size={22} />,
"/inbox": () => <Icons.Inbox2 size={22} />,
Expand All @@ -51,6 +52,10 @@ const defaultItems = [
path: "/tracker",
name: "Tracker",
},
{
path: "/customers",
name: "Customers",
},
{
path: "/vault",
name: "Vault",
Expand Down
123 changes: 123 additions & 0 deletions apps/dashboard/src/components/tables/customers/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"use client";

import { useCustomerParams } from "@/hooks/use-customer-params";
import { Avatar, AvatarFallback, AvatarImageNext } from "@midday/ui/avatar";
import { Button } from "@midday/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@midday/ui/dropdown-menu";
import { DotsHorizontalIcon } from "@radix-ui/react-icons";
import type { ColumnDef } from "@tanstack/react-table";
import * as React from "react";

export type Customer = {
id: string;
name: string;
customer_name?: string;
website: string;
contact_person?: string;
email: string;
invoices: number;
projects: number;
tags: { id: string; name: string }[];
};

export const columns: ColumnDef<Customer>[] = [
{
header: "Name",
accessorKey: "name",
cell: ({ row }) => {
const name = row.original.name ?? row.original.customer_name;

if (!name) return "-";

return (
<div className="flex items-center space-x-2">
<Avatar className="size-5">
{row.original.website && (
<AvatarImageNext
src={`https://img.logo.dev/${row.original.website}?token=pk_X-1ZO13GSgeOoUrIuJ6GMQ&size=60`}
alt={`${name} logo`}
width={20}
height={20}
quality={100}
/>
)}
<AvatarFallback className="text-[9px] font-medium">
{name?.[0]}
</AvatarFallback>
</Avatar>
<span className="truncate">{name}</span>
</div>
);
},
},
{
header: "Contact person",
accessorKey: "contact_person",
cell: ({ row }) => row.getValue("contact_person") ?? "-",
},
{
header: "Email",
accessorKey: "email",
cell: ({ row }) => row.getValue("email") ?? "-",
},
{
header: "Invoices",
accessorKey: "invoices",
cell: ({ row }) => row.getValue("invoices") ?? "-",
},
{
header: "Projects",
accessorKey: "projects",
cell: ({ row }) => row.getValue("projects") ?? "-",
},
{
header: "Tags",
accessorKey: "tags",
cell: ({ row }) => row.getValue("tags") ?? "-",
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const { setParams } = useCustomerParams();

return (
<div>
<DropdownMenu>
<DropdownMenuTrigger asChild className="relative">
<Button variant="ghost" className="h-8 w-8 p-0">
<DotsHorizontalIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>

<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() =>
setParams({
customerId: row.original.id,
})
}
>
Edit customer
</DropdownMenuItem>

<DropdownMenuItem
onClick={() =>
table.options.meta?.deleteCustomer(row.original.id)
}
className="text-[#FF3638]"
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
},
},
];
57 changes: 57 additions & 0 deletions apps/dashboard/src/components/tables/customers/empty-states.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";

import { useCustomerParams } from "@/hooks/use-customer-params";
import { Button } from "@midday/ui/button";

export function EmptyState() {
const { setParams } = useCustomerParams();

return (
<div className="flex items-center justify-center ">
<div className="flex flex-col items-center mt-40">
<div className="text-center mb-6 space-y-2">
<h2 className="font-medium text-lg">No customers</h2>
<p className="text-[#606060] text-sm">
You haven't created any customers yet. <br />
Go ahead and create your first one.
</p>
</div>

<Button
variant="outline"
onClick={() =>
setParams({
createCustomer: true,
})
}
>
Create customer
</Button>
</div>
</div>
);
}

export function NoResults() {
const { setParams } = useCustomerParams();

return (
<div className="flex items-center justify-center ">
<div className="flex flex-col items-center mt-40">
<div className="text-center mb-6 space-y-2">
<h2 className="font-medium text-lg">No results</h2>
<p className="text-[#606060] text-sm">
Try another search, or adjusting the filters
</p>
</div>

<Button
variant="outline"
onClick={() => setParams(null, { shallow: false })}
>
Clear filters
</Button>
</div>
</div>
);
}
64 changes: 64 additions & 0 deletions apps/dashboard/src/components/tables/customers/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { getCustomers } from "@midday/supabase/cached-queries";
import { EmptyState, NoResults } from "./empty-states";
import { DataTable } from "./table";

type Props = {
page: number;
query?: string | null;
sort?: string[] | null;
start?: string | null;
end?: string | null;
};

const pageSize = 25;

export async function CustomersTable({ query, sort, start, end, page }: Props) {
const filter = {
start,
end,
};

async function loadMore({ from, to }: { from: number; to: number }) {
"use server";

return getCustomers({
// to,
// from: from + 1,
// searchQuery: query,
// sort,
// filter,
});
}

const { data, meta } = await getCustomers({
searchQuery: query,
sort,
filter,
to: pageSize,
});

const hasNextPage = Boolean(
meta?.count && meta.count / (page + 1) > pageSize,
);

if (!data?.length) {
if (
query?.length ||
Object.values(filter).some((value) => value !== null)
) {
return <NoResults />;
}

return <EmptyState />;
}

return (
<DataTable
data={data}
// loadMore={loadMore}
pageSize={pageSize}
hasNextPage={hasNextPage}
page={page}
/>
);
}
33 changes: 33 additions & 0 deletions apps/dashboard/src/components/tables/customers/row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client";

import { cn } from "@midday/ui/cn";
import { TableCell, TableRow } from "@midday/ui/table";
import { type Row, flexRender } from "@tanstack/react-table";
import type { Customer } from "./columns";

type Props = {
row: Row<Customer>;
setOpen: (id?: string) => void;
};

export function CustomerRow({ row, setOpen }: Props) {
return (
<>
<TableRow
className="hover:bg-transparent cursor-default h-[45px] cursor-pointer"
key={row.id}
>
{row.getVisibleCells().map((cell, index) => (
<TableCell
key={cell.id}
onClick={() =>
index !== row.getVisibleCells().length - 1 && setOpen(row.id)
}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
</>
);
}
3 changes: 3 additions & 0 deletions apps/dashboard/src/components/tables/customers/skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function CustomersSkeleton() {
return <div>CustomersSkeleton</div>;
}
Loading

0 comments on commit 5856ce1

Please sign in to comment.