From ae260c98d1abf8b0b580d2c770df315248a64d39 Mon Sep 17 00:00:00 2001 From: Jakob Stechow Date: Mon, 20 May 2024 12:03:41 +0200 Subject: [PATCH] wip --- backend/package.json | 1 + backend/src/auth/auth.controller.ts | 6 +- backend/src/auth/auth.service.ts | 9 +- backend/src/db/functions/task.ts | 27 +++- backend/src/task.controller.ts | 26 +++- frontend/app.json | 14 +- frontend/eas.json | 18 +++ frontend/src/components/user-dropdown.tsx | 16 +- frontend/src/components/user-multi-select.tsx | 42 ++--- frontend/src/screens/create-task-group.tsx | 10 +- frontend/src/screens/create-task.tsx | 147 ++++++++++++++---- frontend/src/screens/login.tsx | 2 - frontend/src/utils/fetchWrapper.ts | 24 +-- frontend/src/utils/interval.ts | 8 - 14 files changed, 245 insertions(+), 105 deletions(-) create mode 100644 frontend/eas.json delete mode 100644 frontend/src/utils/interval.ts diff --git a/backend/package.json b/backend/package.json index 8d15157..2eb5884 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,6 +13,7 @@ "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "types": "tsc --noEmit", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 5e3c787..39b5549 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -10,7 +10,7 @@ import { AuthGuard } from '@nestjs/passport'; import * as bcrypt from 'bcrypt'; import { db } from 'src/db'; import { userTable } from 'src/db/schema'; -import { AuthService } from './auth.service'; +import { AuthService, User } from './auth.service'; import { Public } from './public.decorators'; class RegisterDto { @@ -28,9 +28,7 @@ export class AuthController { @Public() @UseGuards(AuthGuard('local')) @Post('login') - //TODO: Fix typing - async login(@Request() req: { user: any }) { - console.log({ user: req.user }); + async login(@Request() req: { user: User }) { return this.authService.login(req.user); } diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 982eb9f..42f47db 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -4,15 +4,16 @@ import * as bcrypt from 'bcrypt'; import { findUserByName } from 'src/db/functions/user'; export type User = { - id: string; - userName: string; + id: number; + username: string; + email: string; }; @Injectable() export class AuthService { constructor(private jwtService: JwtService) {} - async validateUser(username: string, password: string): Promise { + async validateUser(username: string, password: string): Promise { const user = await findUserByName(username); if (user && (await bcrypt.compare(password, user.password))) { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -23,7 +24,7 @@ export class AuthService { } async login(user: User) { - const payload = { username: user.userName, sub: user.id }; + const payload = { username: user.username, sub: user.id }; return { access_token: this.jwtService.sign(payload), }; diff --git a/backend/src/db/functions/task.ts b/backend/src/db/functions/task.ts index aad45bc..f403fa2 100644 --- a/backend/src/db/functions/task.ts +++ b/backend/src/db/functions/task.ts @@ -1,7 +1,7 @@ import { eq } from 'drizzle-orm'; -import { CreateTask, UpdateTask } from 'src/task.controller'; +import { CreateTask, OneOffTask, UpdateTask } from 'src/task.controller'; import { db } from '..'; -import { SelectTask, taskTable } from '../schema'; +import { SelectTask, assignmentTable, taskTable } from '../schema'; export async function dbGetAllTasks(): Promise { return await db.select().from(taskTable); @@ -20,7 +20,7 @@ export async function dbGetTaskById(taskId: number) { } } -export async function dbCreateTask({ +export async function dbCreateRecurringTask({ title, description, taskGroupId, @@ -53,3 +53,24 @@ export async function dbUpdateTask({ throw error; } } + +export async function dbCreateOneOffTask({ + title, + description, + userIds, +}: OneOffTask) { + const tasks = await db + .insert(taskTable) + .values({ title, description }) + .returning({ taskId: taskTable.id }); + const task = tasks[0]; + + const hydratedAssignments = userIds.map((userId) => { + return { + taskId: task.taskId, + userId, + }; + }); + + await db.insert(assignmentTable).values(hydratedAssignments); +} diff --git a/backend/src/task.controller.ts b/backend/src/task.controller.ts index 2532785..976686d 100644 --- a/backend/src/task.controller.ts +++ b/backend/src/task.controller.ts @@ -1,6 +1,11 @@ import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'; import { SelectTask } from './db/schema'; -import { dbCreateTask, dbGetAllTasks, dbUpdateTask } from './db/functions/task'; +import { + dbCreateOneOffTask, + dbCreateRecurringTask, + dbGetAllTasks, + dbUpdateTask, +} from './db/functions/task'; export type CreateTask = { title: string; @@ -8,6 +13,13 @@ export type CreateTask = { taskGroupId?: number; }; +// TODO: extra type for this feels weird +export type OneOffTask = { + title: string; + description: string; + userIds: number[]; +}; + // todo: this shouldnt be a seperate type export type UpdateTask = { title: string; @@ -24,10 +36,10 @@ export class TasksController { return tasks; } - @Post() - async createTask(@Body() task: CreateTask) { + @Post('/recurring') + async createRecurringTask(@Body() task: CreateTask) { console.log({ task }); - await dbCreateTask(task); + await dbCreateRecurringTask(task); } @Put(':id') @@ -35,4 +47,10 @@ export class TasksController { console.log({ updatedTask: task }); await dbUpdateTask({ ...task, id: Number(id) }); } + + @Post('/one-off') + async createOneOffTask(@Body() oneOffTask: OneOffTask) { + console.log(oneOffTask); + await dbCreateOneOffTask(oneOffTask); + } } diff --git a/frontend/app.json b/frontend/app.json index 7ec0346..912fb63 100644 --- a/frontend/app.json +++ b/frontend/app.json @@ -1,6 +1,11 @@ { "expo": { - "name": "frontend", + "platforms": [ + "web", + "android", + "ios" + ], + "name": "WG-Tasks", "slug": "frontend", "version": "1.0.0", "orientation": "portrait", @@ -29,6 +34,11 @@ }, "plugins": [ "expo-secure-store" - ] + ], + "extra": { + "eas": { + "projectId": "4c4ae101-99c9-4338-a54f-3a4c2b35d0fc" + } + } } } diff --git a/frontend/eas.json b/frontend/eas.json new file mode 100644 index 0000000..511527f --- /dev/null +++ b/frontend/eas.json @@ -0,0 +1,18 @@ +{ + "cli": { + "version": ">= 9.0.7" + }, + "build": { + "development": { + "developmentClient": true, + "distribution": "internal" + }, + "preview": { + "distribution": "internal" + }, + "production": {} + }, + "submit": { + "production": {} + } +} diff --git a/frontend/src/components/user-dropdown.tsx b/frontend/src/components/user-dropdown.tsx index a649185..a4cfc30 100644 --- a/frontend/src/components/user-dropdown.tsx +++ b/frontend/src/components/user-dropdown.tsx @@ -13,13 +13,13 @@ const UserDropdown = ({ data, selectedUserId, onChange }: Props) => { const [isFocus, setIsFocus] = useState(false); return ( - + { }} renderLeftIcon={() => ( { export default UserDropdown; -const styles = StyleSheet.create({ +export const dropdownStyles = StyleSheet.create({ container: { padding: 0, }, diff --git a/frontend/src/components/user-multi-select.tsx b/frontend/src/components/user-multi-select.tsx index 7c36600..b3ea1c9 100644 --- a/frontend/src/components/user-multi-select.tsx +++ b/frontend/src/components/user-multi-select.tsx @@ -3,30 +3,34 @@ import { View, Text, TouchableOpacity, StyleSheet } from "react-native"; import { MultiSelect } from "react-native-element-dropdown"; import { Ionicons } from "@expo/vector-icons"; -function renderItem(item: { value: string; id: number }) { +type MultiSelectItemProps = { + username: string; +}; + +function MultiSelectItem({ username }: MultiSelectItemProps) { return ( - {item.value} + {username} ); } -type Props = { - values: { value: string; id: number }[]; - selectedValues: string[]; - setSelectedValues: React.Dispatch>; +type MultiSelectProps = { + users: { username: string; id: number }[]; + selectedUserIds: number[]; + setSelectedUserIds: React.Dispatch>; header: string; }; -export default function CustomMultiSelect({ - values, - setSelectedValues, - selectedValues, +export default function UserMultiSelect({ + users, + setSelectedUserIds: setSelectedValues, + selectedUserIds: selectedValues, header, -}: Props) { +}: MultiSelectProps) { return ( - + {header} value.toString())} activeColor="#9bd4e4" search searchPlaceholder="Search..." - onChange={(value) => { - setSelectedValues(value); + onChange={(values) => { + setSelectedValues(values.map((value) => Number(value))); }} renderLeftIcon={() => ( )} - renderItem={renderItem} + renderItem={MultiSelectItem} renderSelectedItem={(item, unSelect) => ( unSelect && unSelect(item)}> - {item.value} + {item.username} diff --git a/frontend/src/screens/create-task-group.tsx b/frontend/src/screens/create-task-group.tsx index 2f0d9f6..23269b0 100644 --- a/frontend/src/screens/create-task-group.tsx +++ b/frontend/src/screens/create-task-group.tsx @@ -11,7 +11,7 @@ import Loading from "../components/loading"; import WebDateTimerPicker from "../components/web-date-picker"; import { fetchWrapper } from "../utils/fetchWrapper"; import { getUsers } from "./assignments"; -import CustomMultiSelect from "../components/user-multi-select"; +import UserMultiSelect from "../components/user-multi-select"; const createTaskGroupSchema = z.object({ title: z.string().min(1, { message: "Title is missing" }), @@ -168,10 +168,10 @@ export function CreateTaskGroupScreen() { Interval is required )} - {/* TODO: When inserting a date into the database, it somehow is one day earlier in the database. For example inserting 31.05.2024 -> 30.05.2024 in db diff --git a/frontend/src/screens/create-task.tsx b/frontend/src/screens/create-task.tsx index 37eebeb..e6f3362 100644 --- a/frontend/src/screens/create-task.tsx +++ b/frontend/src/screens/create-task.tsx @@ -14,31 +14,62 @@ import { z } from "zod"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import Toast from "react-native-toast-message"; import { fetchWrapper } from "../utils/fetchWrapper"; -import CustomMultiSelect from "../components/user-multi-select"; +import { Dropdown } from "react-native-element-dropdown"; +import { dropdownStyles } from "../components/user-dropdown"; +import UserMultiSelect from "../components/user-multi-select"; +import { getUsers } from "./assignments"; +import Loading from "../components/loading"; -const createTaskSchema = z.object({ +const createRecurringTaskSchema = z.object({ title: z.string().min(1, { message: "Title is missing" }), description: z.string().optional(), + taskGroupId: z.number().optional(), }); -type CreateTask = z.infer; +type CreateRecurringTask = z.infer; +const createOneOffTaskSchema = z.object({ + title: z.string().min(1, { message: "Title is missing" }), + description: z.string().optional(), + userIds: z.number().array(), +}); + +type CreateOneOffTask = z.infer; + +async function createOneOffTask({ + title, + description, + userIds, +}: CreateOneOffTask) { + await fetchWrapper.post("/tasks/one-off/", { + title, + description, + userIds, + }); +} + +// TODO: Should be moved inside task group list screen when it exists const taskGroupSchema = z.object({ id: z.number(), title: z.string(), }); -async function createTask({ title, description }: CreateTask) { - await fetchWrapper.post("tasks", { +async function createRecurringTask({ + title, + description, + taskGroupId, +}: CreateRecurringTask) { + await fetchWrapper.post("tasks/recurring", { title, description, + taskGroupId, }); } +// TODO: Should be moved inside task group list screen when it exists async function getTaskGroups() { const response = await fetchWrapper.get("task-group"); const json = await response.json(); - console.log({ json }); const parsed = z.array(taskGroupSchema).parse(json); return parsed; } @@ -49,30 +80,40 @@ const defaultValues = { }; export function CreateTaskScreen() { - const [selectedTaskGroups, setSelectedTaskGroups] = React.useState( - [], - ); + const [selectedTaskGroupId, setSelectedTaskGroupId] = React.useState< + number | undefined + >(undefined); const [taskType, setTaskType] = React.useState<"recurring" | "non-recurring">( "recurring", ); + const [selectedUserIds, setSelectedUserIds] = React.useState([]); + const queryClient = useQueryClient(); const { control, handleSubmit, formState: { errors }, reset: resetForm, - } = useForm({ + } = useForm({ defaultValues, - resolver: zodResolver(createTaskSchema), + resolver: zodResolver(createRecurringTaskSchema), }); const { mutate: createTaskMutation } = useMutation({ - mutationFn: ({ ...args }: CreateTask) => - createTask({ - title: args.title, - description: args.description, - }), + mutationFn: ({ ...args }: CreateRecurringTask) => { + return selectedTaskGroupId === undefined + ? createOneOffTask({ + title: args.title, + description: args.description, + userIds: [], + }) + : createRecurringTask({ + title: args.title, + description: args.description, + taskGroupId: selectedTaskGroupId, + }); + }, onSuccess: () => { Toast.show({ type: "success", text1: "Succcessfully created task" }); resetForm({ ...defaultValues }); @@ -84,30 +125,33 @@ export function CreateTaskScreen() { mutationKey: ["tasks"], }); - const { data: taskGroups, isLoading } = useQuery({ + const { data: taskGroups, isLoading: isTaskGrousLoading } = useQuery({ queryKey: ["taskGroup"], queryFn: getTaskGroups, }); - console.log({ taskGroups }); + const { data: users, isLoading: isUsersLoading } = useQuery({ + queryKey: ["users"], + queryFn: getUsers, + }); - function onSubmit(data: CreateTask) { + function onSubmit(data: CreateRecurringTask) { createTaskMutation({ ...data, }); queryClient.refetchQueries({ queryKey: ["tasks"] }); } - if (isLoading || taskGroups === undefined) { - return null; + if ( + isTaskGrousLoading || + taskGroups === undefined || + isUsersLoading || + users === undefined + ) { + return ; } - const hydratedTaskGroups = taskGroups.map((taskGroup) => { - return { - value: taskGroup.title, - id: taskGroup.id, - }; - }); + const noTaskGroupExist = taskGroups.length === 0; return ( @@ -158,7 +202,13 @@ export function CreateTaskScreen() { )} Options: - Recurring task + + One-off task + @@ -167,13 +217,42 @@ export function CreateTaskScreen() { ) } /> + + Recurring task + - {taskType === "recurring" && ( - + setSelectedTaskGroupId(item.id)} + style={dropdownStyles.dropdown} + placeholderStyle={dropdownStyles.placeholderStyle} + selectedTextStyle={dropdownStyles.selectedTextStyle} + inputSearchStyle={dropdownStyles.inputSearchStyle} + iconStyle={dropdownStyles.iconStyle} + placeholder="Select a task group (optional)" + /> + {noTaskGroupExist && ( + + Currently, there are no task groups available. Please create a + task group first in order to assign tasks to it. + + )} + + ) : ( + )} { const jwtToken = await StorageWrapper.getItem("jwt-token"); - const response = await fetch( - `${process.env.EXPO_PUBLIC_API_URL}/${endpoint}`, - { - headers: { - Authorization: `Bearer ${jwtToken}`, - "Content-Type": "application/json", - ...options?.headers, - }, - method, - body: data ? JSON.stringify(data) : undefined, - ...options, + const response = await fetch(`http://192.168.178.87:3000/${endpoint}`, { + headers: { + Authorization: `Bearer ${jwtToken}`, + "Content-Type": "application/json", + ...options?.headers, }, - ); + method, + body: data ? JSON.stringify(data) : undefined, + ...options, + }); + if (!response.ok) { + throw new Error("response was not ok"); + } return response; }; diff --git a/frontend/src/utils/interval.ts b/frontend/src/utils/interval.ts deleted file mode 100644 index fe7af68..0000000 --- a/frontend/src/utils/interval.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IntervalType } from "../screens/create-task"; - -type IntervalItem = { label: string; value: IntervalType }; -export const intervalItems = [ - { label: "Hours", value: "hours" }, - { label: "Days", value: "days" }, - { label: "Weeks", value: "weeks" }, -] satisfies IntervalItem[];