Skip to content

Commit

Permalink
Merge pull request #110 from midday-ai/feature/user-categories
Browse files Browse the repository at this point in the history
Feature/user categories
  • Loading branch information
pontusab committed May 13, 2024
2 parents d3b3039 + 2445e8c commit 662f697
Show file tree
Hide file tree
Showing 96 changed files with 3,829 additions and 1,852 deletions.
8 changes: 4 additions & 4 deletions apps/dashboard/.env-example
Expand Up @@ -36,9 +36,6 @@ NEXT_PUBLIC_PLAID_ENVIRONMENT=
# Github
GITHUB_TOKEN=

#Open API
OPENAI_API_KEY=

#Novu
NEXT_PUBLIC_NOVU_APPLICATION_IDENTIFIER=

Expand All @@ -49,4 +46,7 @@ UNSUBSCRIBE_JWT_ISSUER=
PLAIN_API_KEY=

# Baselime
BASELIME_SERVICE=
BASELIME_SERVICE=

# Groq
GROQ_API_KEY=
24 changes: 13 additions & 11 deletions apps/dashboard/package.json
Expand Up @@ -13,9 +13,11 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@ai-sdk/openai": "^0.0.10",
"@baselime/node-opentelemetry": "^0.5.8",
"@date-fns/utc": "^1.2.0",
"@hookform/resolvers": "^3.3.4",
"@langchain/groq": "^0.0.9",
"@midday/events": "workspace:*",
"@midday/inbox": "workspace:*",
"@midday/jobs": "workspace:*",
Expand All @@ -30,21 +32,21 @@
"@tanstack/react-table": "^8.16.0",
"@team-plain/typescript-sdk": "3.9.0",
"@todesktop/client-active-win": "^0.15.0",
"@todesktop/client-core": "^1.2.5",
"@todesktop/runtime": "^1.6.1",
"@todesktop/client-core": "^1.3.0",
"@todesktop/runtime": "^1.6.2",
"@trigger.dev/nextjs": "^2.3.18",
"@tsparticles/engine": "^3.3.0",
"@tsparticles/react": "^3.0.0",
"@tsparticles/slim": "^3.3.0",
"@uidotdev/usehooks": "^2.4.1",
"@vercel/analytics": "^1.2.2",
"@vercel/speed-insights": "^1.0.10",
"@zip.js/zip.js": "2.7.43",
"ai": "^3.1.1",
"@zip.js/zip.js": "2.7.44",
"ai": "^3.1.5",
"change-case": "^5.4.4",
"currency-symbol-map": "^5.1.0",
"dub": "^0.25.2",
"framer-motion": "^11.1.7",
"framer-motion": "^11.1.9",
"geist": "^1.3.0",
"headless-currency-input": "^1.1.0",
"loops": "1.0.1",
Expand All @@ -54,30 +56,30 @@
"next-international": "1.2.4",
"next-safe-action": "6.2.0",
"next-themes": "^0.3.0",
"nuqs": "^1.17.1",
"openai": "^4.41.0",
"nuqs": "^1.17.2",
"react": "18.3.1",
"react-colorful": "^5.6.1",
"react-dom": "18.3.1",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.51.4",
"react-hotkeys-hook": "^4.5.0",
"react-intersection-observer": "^9.10.2",
"react-pdf": "^8.0.0",
"react-pdf": "^8.0.2",
"react-plaid-link": "^3.5.1",
"recharts": "^2.12.6",
"recharts": "^2.12.7",
"resend": "^3.2.0",
"sharp": "^0.33.3",
"teller-connect-react": "^0.1.0",
"tus-js-client": "^4.1.0",
"use-long-press": "^3.2.0",
"zod": "^3.23.6",
"zod": "^3.23.8",
"zustand": "^4.5.2"
},
"devDependencies": {
"@midday/tsconfig": "workspace:*",
"@t3-oss/env-nextjs": "^0.10.1",
"@todesktop/tailwind-variants": "^1.0.1",
"@types/node": "^20.12.8",
"@types/node": "^20.12.11",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"typescript": "^5.4.5"
Expand Down
40 changes: 40 additions & 0 deletions apps/dashboard/src/actions/ai/get-vat-rate.ts
@@ -0,0 +1,40 @@
"use server";

import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatGroq } from "@langchain/groq";
import { getCountry } from "@midday/location";
import { z } from "zod";
import { action } from "../safe-action";
import { getVatRateSchema } from "../schema";

const model = new ChatGroq({
temperature: 0,
model: "mixtral-8x7b-32768",
apiKey: process.env.GROQ_API_KEY,
});

const vatSchema = z.object({
vat: z.number().min(5).max(100),
});

const modelWithStructuredOutput = model.withStructuredOutput(vatSchema);

export const getVatRateAction = action(getVatRateSchema, async ({ name }) => {
const country = getCountry();

const prompt = ChatPromptTemplate.fromMessages([
[
"system",
"You are an expert in VAT rates for the specific country and category",
],
["human", `What's the VAT rate for category ${name} in ${country.name}?`],
]);

const chain = prompt.pipe(modelWithStructuredOutput);
const result = await chain.invoke({});

return {
vat: result.vat,
country: country.name,
};
});
34 changes: 34 additions & 0 deletions apps/dashboard/src/actions/create-categories-action.ts
@@ -0,0 +1,34 @@
"use server";

import { getUser } from "@midday/supabase/cached-queries";
import { createClient } from "@midday/supabase/server";
import { revalidateTag } from "next/cache";
import { action } from "./safe-action";
import { createCategoriesSchema } from "./schema";

export const createCategoriesAction = action(
createCategoriesSchema,
async ({ categories }) => {
const supabase = createClient();
const user = await getUser();
const teamId = user?.data?.team_id;

const { data, error } = await supabase
.from("transaction_categories")
.insert(
categories.map((category) => ({
...category,
team_id: teamId,
}))
)
.select("id, name, color, vat, slug");

if (error) {
throw Error(error.message);
}

revalidateTag(`transaction_categories_${teamId}`);

return data;
}
);
33 changes: 33 additions & 0 deletions apps/dashboard/src/actions/delete-categories-action.ts
@@ -0,0 +1,33 @@
"use server";

import { getUser } from "@midday/supabase/cached-queries";
import { createClient } from "@midday/supabase/server";
import {
revalidatePath as revalidatePathFunc,
revalidateTag,
} from "next/cache";
import { action } from "./safe-action";
import { deleteCategoriesSchema } from "./schema";

export const deleteCategoriesAction = action(
deleteCategoriesSchema,
async ({ ids, revalidatePath }) => {
const supabase = createClient();
const user = await getUser();

const teamId = user?.data?.team_id;

const response = await supabase
.from("transaction_categories")
.delete()
.in("id", ids)
.eq("system", false)
.select();

revalidatePathFunc(revalidatePath);
revalidateTag(`transactions_${teamId}`);
revalidateTag(`spending_${teamId}`);

return response;
}
);
40 changes: 36 additions & 4 deletions apps/dashboard/src/actions/schema.ts
Expand Up @@ -145,10 +145,10 @@ export const sendFeedbackSchema = z.object({
});

export const updateTransactionSchema = z.object({
id: z.string(),
id: z.string().uuid(),
note: z.string().optional(),
category: z.string().optional(),
assigned_id: z.string().optional(),
category_slug: z.string().optional(),
assigned_id: z.string().uuid().optional(),
status: z.enum(["deleted", "excluded", "posted", "completed"]).optional(),
});

Expand All @@ -158,6 +158,11 @@ export const deleteTransactionSchema = z.object({
ids: z.array(z.string()),
});

export const deleteCategoriesSchema = z.object({
ids: z.array(z.string()),
revalidatePath: z.string(),
});

export const bulkUpdateTransactionsSchema = z.object({
type: z.enum(["category", "note", "assigned", "status"]),
data: z.array(updateTransactionSchema),
Expand Down Expand Up @@ -223,6 +228,29 @@ export type InviteTeamMembersFormValues = z.infer<
typeof inviteTeamMembersSchema
>;

export const createCategoriesSchema = z.object({
categories: z.array(
z.object({
name: z.string().optional(),
description: z.string().optional(),
color: z.string().optional(),
vat: z.string().optional(),
})
),
});

export type CreateCategoriesFormValues = z.infer<typeof createCategoriesSchema>;

export const updateCategorySchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
color: z.string(),
description: z.string().optional().nullable(),
vat: z.string().optional().nullable(),
});

export type UpdateCategoriesFormValues = z.infer<typeof updateCategorySchema>;

export const deleteInviteSchema = z.object({
id: z.string(),
revalidatePath: z.string().optional(),
Expand Down Expand Up @@ -333,8 +361,12 @@ export const verifyOtpSchema = z.object({

export const searchSchema = z.object({
query: z.string().min(1),
type: z.enum(["inbox"]),
type: z.enum(["inbox", "categories"]),
limit: z.number().optional(),
});

export const inboxOrder = z.boolean();

export const getVatRateSchema = z.object({
name: z.string().min(2),
});
15 changes: 14 additions & 1 deletion apps/dashboard/src/actions/search-action.ts
@@ -1,9 +1,9 @@
"use server";

import { getUser } from "@midday/supabase/cached-queries";
import { createClient } from "@midday/supabase/server";
import { action } from "./safe-action";
import { searchSchema } from "./schema";
import { createClient } from "@midday/supabase/server";

export const searchAction = action(searchSchema, async (params) => {
const user = await getUser();
Expand Down Expand Up @@ -34,6 +34,19 @@ export const searchAction = action(searchSchema, async (params) => {
return data;
}

case "categories": {
const query = supabase
.from("transaction_categories")
.select("id, name, color, slug")
.eq("team_id", teamId)
.ilike("name", `%${searchQuery}%`)
.order("created_at", { ascending: true });

const { data } = await query.range(0, limit);

return data;
}

default:
return [];
}
Expand Down
10 changes: 5 additions & 5 deletions apps/dashboard/src/actions/sign-out-action.ts
Expand Up @@ -8,16 +8,16 @@ import { revalidateTag } from "next/cache";
export async function signOutAction() {
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
data: { session },
} = await supabase.auth.getSession();

await supabase.auth.signOut({
scope: "local",
});

const logsnag = await setupLogSnag({
userId: user.id,
fullName: user.full_name,
userId: session?.user.id,
fullName: session?.user.user_metadata?.full_name,
});

logsnag.track({
Expand All @@ -26,5 +26,5 @@ export async function signOutAction() {
channel: LogEvents.SignOut.channel,
});

revalidateTag(`user_${user.id}`);
revalidateTag(`user_${session?.user.id}`);
}
25 changes: 25 additions & 0 deletions apps/dashboard/src/actions/update-category-action.ts
@@ -0,0 +1,25 @@
"use server";

import { getUser } from "@midday/supabase/cached-queries";
import { createClient } from "@midday/supabase/server";
import { revalidateTag } from "next/cache";
import { action } from "./safe-action";
import { updateCategorySchema } from "./schema";

export const updateCategoryAction = action(
updateCategorySchema,
async ({ id, name, color, description, vat }) => {
const supabase = createClient();
const user = await getUser();
const teamId = user?.data.team_id;

await supabase
.from("transaction_categories")
.update({ name, color, description, vat })
.eq("id", id);

revalidateTag(`transaction_categories_${teamId}`);
revalidateTag(`transactions_${teamId}`);
revalidateTag(`spending_${teamId}`);
}
);
Expand Up @@ -14,7 +14,14 @@ export const updateSimilarTransactionsAction = action(
const user = await getUser();
const teamId = user?.data?.team_id;

await updateSimilarTransactions(supabase, id);
if (!teamId) {
return null;
}

await updateSimilarTransactions(supabase, {
team_id: teamId,
id,
});

revalidateTag(`transactions_${teamId}`);
revalidateTag(`spending_${teamId}`);
Expand Down

0 comments on commit 662f697

Please sign in to comment.