From e3274cdecf54dcc2db0da1373845e167d7532fd8 Mon Sep 17 00:00:00 2001 From: William Petit Date: Mon, 14 Sep 2020 08:11:42 +0200 Subject: [PATCH] feat(ui+backend): task categories edition --- client/src/gql/fragments/project.ts | 13 +- client/src/gql/mutations/project.tsx | 38 ++++- client/src/hooks/useProjectReducer.sagas.ts | 110 ++++++++++++- client/src/hooks/useProjectReducer.ts | 164 ++++++++++++-------- internal/graph/error.go | 14 ++ internal/graph/mutation.graphql | 8 + internal/graph/mutation.resolvers.go | 12 ++ internal/graph/project_handler.go | 55 +++++++ internal/model/error.go | 7 + internal/model/project_repository.go | 104 ++++++++++++- 10 files changed, 449 insertions(+), 76 deletions(-) create mode 100644 internal/graph/error.go create mode 100644 internal/model/error.go diff --git a/client/src/gql/fragments/project.ts b/client/src/gql/fragments/project.ts index d1174f8..97ca6fd 100644 --- a/client/src/gql/fragments/project.ts +++ b/client/src/gql/fragments/project.ts @@ -27,14 +27,20 @@ fragment FullProjectParams on ProjectParams { } `; +export const FRAGMENT_FULL_TASK_CATEGORY = gql` +fragment FullTaskCategory on TaskCategory { + id + label + costPerTimeUnit +} +`; + export const FRAGMENT_FULL_PROJECT = gql` fragment FullProject on Project { id title taskCategories { - id - label - costPerTimeUnit + ...FullTaskCategory } tasks { ...FullTask @@ -45,4 +51,5 @@ fragment FullProject on Project { } ${FRAGMENT_FULL_TASK} ${FRAGMENT_FULL_PROJECT_PARAMS} +${FRAGMENT_FULL_TASK_CATEGORY} ` \ No newline at end of file diff --git a/client/src/gql/mutations/project.tsx b/client/src/gql/mutations/project.tsx index 536e064..1341c80 100644 --- a/client/src/gql/mutations/project.tsx +++ b/client/src/gql/mutations/project.tsx @@ -1,5 +1,5 @@ import { gql, useMutation, PureQueryOptions } from '@apollo/client'; -import { FRAGMENT_FULL_PROJECT, FRAGMENT_FULL_TASK, FRAGMENT_FULL_PROJECT_PARAMS } from '../fragments/project'; +import { FRAGMENT_FULL_PROJECT, FRAGMENT_FULL_TASK, FRAGMENT_FULL_PROJECT_PARAMS, FRAGMENT_FULL_TASK_CATEGORY } from '../fragments/project'; import { QUERY_PROJECTS } from '../queries/project'; export const MUTATION_CREATE_PROJECT = gql` @@ -82,3 +82,39 @@ ${FRAGMENT_FULL_PROJECT_PARAMS} export function useUpdateProjectParamsMutation() { return useMutation(MUTATION_UPDATE_PROJECT_PARAMS); } + +export const MUTATION_ADD_PROJECT_TASK_CATEGORY = gql` +mutation addProjectTaskCategory($projectId: ID!, $changes: ProjectTaskCategoryChanges!) { + addProjectTaskCategory(projectId: $projectId, changes: $changes) { + ...FullTaskCategory + } +} +${FRAGMENT_FULL_TASK_CATEGORY} +`; + +export function useAddProjectTaskCategoryMutation() { + return useMutation(MUTATION_ADD_PROJECT_TASK_CATEGORY); +} + +export const MUTATION_UPDATE_PROJECT_TASK_CATEGORY = gql` +mutation updateProjectTaskCategory($projectId: ID!, $taskCategoryId: ID!, $changes: ProjectTaskCategoryChanges!) { + updateProjectTaskCategory(projectId: $projectId, taskCategoryId: $taskCategoryId, changes: $changes) { + ...FullTaskCategory + } +} +${FRAGMENT_FULL_TASK_CATEGORY} +`; + +export function useUpdateProjectTaskCategoryMutation() { + return useMutation(MUTATION_UPDATE_PROJECT_TASK_CATEGORY); +} + +export const MUTATION_REMOVE_PROJECT_TASK_CATEGORY = gql` +mutation removeProjectTaskCategory($projectId: ID!, $taskCategoryId: ID!) { + removeProjectTaskCategory(projectId: $projectId, taskCategoryId: $taskCategoryId) +} +`; + +export function useRemoveProjectTaskCategoryMutation() { + return useMutation(MUTATION_REMOVE_PROJECT_TASK_CATEGORY); +} diff --git a/client/src/hooks/useProjectReducer.sagas.ts b/client/src/hooks/useProjectReducer.sagas.ts index b50b62c..78508e4 100644 --- a/client/src/hooks/useProjectReducer.sagas.ts +++ b/client/src/hooks/useProjectReducer.sagas.ts @@ -1,7 +1,22 @@ import { all, select, takeLatest, put, delay } from "redux-saga/effects"; import { client } from '../gql/client'; -import { MUTATION_CREATE_PROJECT, MUTATION_UPDATE_PROJECT_TITLE, MUTATION_ADD_PROJECT_TASK, MUTATION_REMOVE_PROJECT_TASK, MUTATION_UPDATE_PROJECT_TASK, MUTATION_UPDATE_PROJECT_PARAMS } from "../gql/mutations/project"; -import { UPDATE_PROJECT_TITLE, resetProject, ADD_TASK, taskSaved, AddTask, taskRemoved, RemoveTask, REMOVE_TASK, UPDATE_TASK_ESTIMATION, updateTaskEstimation, UpdateTaskEstimation, UpdateTaskLabel, UPDATE_TASK_LABEL, UpdateParam, paramsSaved, UPDATE_PARAM } from "./useProjectReducer"; +import { + MUTATION_CREATE_PROJECT, MUTATION_UPDATE_PROJECT_TITLE, + MUTATION_ADD_PROJECT_TASK, MUTATION_REMOVE_PROJECT_TASK, + MUTATION_UPDATE_PROJECT_TASK, MUTATION_UPDATE_PROJECT_PARAMS, + MUTATION_ADD_PROJECT_TASK_CATEGORY, + MUTATION_UPDATE_PROJECT_TASK_CATEGORY, + MUTATION_REMOVE_PROJECT_TASK_CATEGORY +} from "../gql/mutations/project"; +import { + UPDATE_PROJECT_TITLE, resetProject, + ADD_TASK, taskSaved, AddTask, + taskRemoved, RemoveTask, REMOVE_TASK, + UPDATE_TASK_ESTIMATION, UpdateTaskEstimation, + UpdateTaskLabel, UPDATE_TASK_LABEL, UpdateParam, + paramsSaved, UPDATE_PARAM, AddTaskCategory, + ADD_TASK_CATEGORY, taskCategorySaved, UpdateTaskCategoryLabel, UPDATE_TASK_CATEGORY_LABEL, UPDATE_TASK_CATEGORY_COST, updateTaskCategoryCost, UpdateTaskCategoryCost, RemoveTaskCategory, taskCategoryRemoved, REMOVE_TASK_CATEGORY, backendError +} from "./useProjectReducer"; import { Project } from "../types/project"; export function* rootSaga() { @@ -13,6 +28,10 @@ export function* rootSaga() { takeLatest(UPDATE_PARAM, updateProjectParamsSaga), takeLatest(ADD_TASK, addTaskSaga), takeLatest(REMOVE_TASK, removeTaskSaga), + takeLatest(ADD_TASK_CATEGORY, addProjectTaskCategorySaga), + takeLatest(UPDATE_TASK_CATEGORY_LABEL, updateProjectTaskCategoryLabelSaga), + takeLatest(UPDATE_TASK_CATEGORY_COST, updateProjectTaskCategoryCostSaga), + takeLatest(REMOVE_TASK_CATEGORY, removeProjectTaskCategorySaga), ]); } @@ -163,4 +182,91 @@ export function* updateProjectParamsSaga({ name, value }: UpdateParam) { }); yield put(paramsSaved({ ...data.updateProjectParams })); +} + +export function* addProjectTaskCategorySaga({ taskCategory }: AddTaskCategory) { + let project: Project = yield select(); + + if (project.id === undefined) { + project = yield createProjectSaga(); + } + + const { data } = yield client.mutate({ + mutation: MUTATION_ADD_PROJECT_TASK_CATEGORY, + variables: { + projectId: project.id, + changes: { + label: taskCategory.label, + costPerTimeUnit: taskCategory.costPerTimeUnit, + } + } + }); + + yield put(taskCategorySaved({ ...data.addProjectTaskCategory })); +} + +export function* updateProjectTaskCategoryLabelSaga({ categoryId, label }: UpdateTaskCategoryLabel) { + let project: Project = yield select(); + + if (project.id === undefined) { + project = yield createProjectSaga(); + } + + const { data } = yield client.mutate({ + mutation: MUTATION_UPDATE_PROJECT_TASK_CATEGORY, + variables: { + projectId: project.id, + taskCategoryId: categoryId, + changes: { + label, + } + } + }); + + yield put(taskCategorySaved({ ...data.updateProjectTaskCategory })); +} + +export function* updateProjectTaskCategoryCostSaga({ categoryId, costPerTimeUnit }: UpdateTaskCategoryCost) { + let project: Project = yield select(); + + if (project.id === undefined) { + project = yield createProjectSaga(); + } + + const { data } = yield client.mutate({ + mutation: MUTATION_UPDATE_PROJECT_TASK_CATEGORY, + variables: { + projectId: project.id, + taskCategoryId: categoryId, + changes: { + costPerTimeUnit, + } + } + }); + + yield put(taskCategorySaved({ ...data.updateProjectTaskCategory })); +} + +export function* removeProjectTaskCategorySaga({ taskCategoryId }: RemoveTaskCategory) { + let project: Project = yield select(); + + if (project.id === undefined) { + project = yield createProjectSaga(); + } + + try { + yield client.mutate({ + mutation: MUTATION_REMOVE_PROJECT_TASK_CATEGORY, + variables: { + projectId: project.id, + taskCategoryId, + } + }); + } catch(err) { + yield put(backendError(err)); + return + } + + + yield put(taskCategoryRemoved(taskCategoryId)); } \ No newline at end of file diff --git a/client/src/hooks/useProjectReducer.ts b/client/src/hooks/useProjectReducer.ts index b8f6fc3..6f1e841 100644 --- a/client/src/hooks/useProjectReducer.ts +++ b/client/src/hooks/useProjectReducer.ts @@ -5,6 +5,10 @@ import { rootSaga } from "./useProjectReducer.sagas"; import { uuid } from "../util/uuid"; import { Params } from "../types/params"; +export interface ProjectState extends Project { + lastBackendError: Error +} + export interface Action { type: string } @@ -23,13 +27,16 @@ UpdateTaskCategoryLabel | UpdateTaskCategoryCost | AddTaskCategory | RemoveTaskCategory | -ResetProject +TaskCategoryRemoved | +TaskCategorySaved | +ResetProject | +BackendError export function useProjectReducer(project: Project) { - return useReducerAndSaga(projectReducer, project, rootSaga); + return useReducerAndSaga(projectReducer, project, rootSaga); } -export function projectReducer(project: Project, action: ProjectReducerActions): Project { +export function projectReducer(project: ProjectState, action: ProjectReducerActions): ProjectState { console.log(action.type, action); switch(action.type) { @@ -50,21 +57,21 @@ export function projectReducer(project: Project, action: ProjectReducerActions): case UPDATE_PARAM: return handleUpdateParam(project, action as UpdateParam); - - case ADD_TASK_CATEGORY: - return handleAddTaskCategory(project, action as AddTaskCategory); - - case REMOVE_TASK_CATEGORY: - return handleRemoveTaskCategory(project, action as RemoveTaskCategory); - - case UPDATE_TASK_CATEGORY_LABEL: - return handleUpdateTaskCategoryLabel(project, action as UpdateTaskCategoryLabel); - - case UPDATE_TASK_CATEGORY_COST: - return handleUpdateTaskCategoryCost(project, action as UpdateTaskCategoryCost); + + case PARAMS_SAVED: + return handleParamsSaved(project, action as ParamsSaved); + + case TASK_CATEGORY_SAVED: + return handleTaskCategorySaved(project, action as TaskCategorySaved); + + case TASK_CATEGORY_REMOVED: + return handleTaskCategoryRemoved(project, action as TaskCategoryRemoved); case RESET_PROJECT: return handleResetProject(project, action as ResetProject); + + case BACKEND_ERROR: + return handleBackendError(project, action as BackendError); } @@ -95,7 +102,7 @@ export function taskSaved(task: Task): TaskSaved { return { type: TASK_SAVED, task }; } -export function handleTaskSaved(project: Project, action: TaskSaved): Project { +export function handleTaskSaved(project: ProjectState, action: TaskSaved): ProjectState { const taskIndex = project.tasks.findIndex(t => t.id === action.task.id); const tasks = [ ...project.tasks ]; if (taskIndex === -1) { @@ -130,7 +137,7 @@ export function taskRemoved(id: number): TaskRemoved { } -export function handleTaskRemoved(project: Project, action: TaskRemoved): Project { +export function handleTaskRemoved(project: ProjectState, action: TaskRemoved): ProjectState { const tasks = [...project.tasks]; const taskIndex = project.tasks.findIndex(t => t.id === action.id); if (taskIndex === -1) return project; @@ -153,7 +160,7 @@ export function updateTaskEstimation(id: number, confidence: EstimationConfidenc return { type: UPDATE_TASK_ESTIMATION, id, confidence, value }; } -export function handleUpdateTaskEstimation(project: Project, action: UpdateTaskEstimation): Project { +export function handleUpdateTaskEstimation(project: ProjectState, action: UpdateTaskEstimation): ProjectState { const tasks = [...project.tasks]; const taskIndex = project.tasks.findIndex(t => t.id === action.id); if (taskIndex === -1) return project; @@ -182,7 +189,7 @@ export function updateProjectTitle(title: string): UpdateProjectTitle { return { type: UPDATE_PROJECT_TITLE, title }; } -export function handleUpdateProjectTitle(project: Project, action: UpdateProjectTitle): Project { +export function handleUpdateProjectTitle(project: ProjectState, action: UpdateProjectTitle): ProjectState { return { ...project, title: action.title @@ -200,7 +207,7 @@ export function updateTaskLabel(id: number, label: string): UpdateTaskLabel { return { type: UPDATE_TASK_LABEL, id, label }; } -export function handleUpdateTaskLabel(project: Project, action: UpdateTaskLabel): Project { +export function handleUpdateTaskLabel(project: ProjectState, action: UpdateTaskLabel): ProjectState { const tasks = [...project.tasks]; const taskIndex = project.tasks.findIndex(t => t.id === action.id); if (taskIndex === -1) return project; @@ -224,7 +231,7 @@ export function updateParam(name: string, value: any): UpdateParam { return { type: UPDATE_PARAM, name, value }; } -export function handleUpdateParam(project: Project, action: UpdateParam): Project { +export function handleUpdateParam(project: ProjectState, action: UpdateParam): ProjectState { return { ...project, params: { @@ -244,6 +251,41 @@ export function paramsSaved(params: Params): ParamsSaved { return { type: PARAMS_SAVED, params }; } +function handleParamsSaved(project: ProjectState, action: ParamsSaved): ProjectState { + return { + ...project, + params: { + ...action.params + } + } +} + +export interface TaskCategorySaved extends Action { + taskCategory: TaskCategory +} + +export const TASK_CATEGORY_SAVED = "TASK_CATEGORY_SAVED"; + +export function taskCategorySaved(taskCategory: TaskCategory): TaskCategorySaved { + return { type: TASK_CATEGORY_SAVED, taskCategory }; +} + +export function handleTaskCategorySaved(project: ProjectState, action: TaskCategorySaved): ProjectState { + const taskCategories = [...project.taskCategories]; + const taskCategoryIndex = taskCategories.findIndex(tc => tc.id === action.taskCategory.id); + + if (taskCategoryIndex === -1) { + taskCategories.push({ ...action.taskCategory }); + } else { + taskCategories[taskCategoryIndex] = { ...action.taskCategory }; + } + + return { + ...project, + taskCategories + }; +} + export interface UpdateTaskCategoryLabel extends Action { categoryId: TaskCategoryID label: string @@ -255,19 +297,6 @@ export function updateTaskCategoryLabel(categoryId: TaskCategoryID, label: strin return { type: UPDATE_TASK_CATEGORY_LABEL, categoryId, label }; } -export function handleUpdateTaskCategoryLabel(project: Project, action: UpdateTaskCategoryLabel): Project { - return { - ...project, - taskCategories: { - ...project.taskCategories, - [action.categoryId]: { - ...project.taskCategories[action.categoryId], - label: action.label - }, - } - }; -} - export interface UpdateTaskCategoryCost extends Action { categoryId: TaskCategoryID costPerTimeUnit: number @@ -279,19 +308,6 @@ export function updateTaskCategoryCost(categoryId: TaskCategoryID, costPerTimeUn return { type: UPDATE_TASK_CATEGORY_COST, categoryId, costPerTimeUnit }; } -export function handleUpdateTaskCategoryCost(project: Project, action: UpdateTaskCategoryCost): Project { - return { - ...project, - taskCategories: { - ...project.taskCategories, - [action.categoryId]: { - ...project.taskCategories[action.categoryId], - costPerTimeUnit: action.costPerTimeUnit - }, - } - }; -} - export const ADD_TASK_CATEGORY = "ADD_TASK_CATEGORY"; export interface AddTaskCategory extends Action { @@ -302,17 +318,6 @@ export function addTaskCategory(taskCategory: TaskCategory): AddTaskCategory { return { type: ADD_TASK_CATEGORY, taskCategory }; } -export function handleAddTaskCategory(project: Project, action: AddTaskCategory): Project { - const taskCategory = { ...action.taskCategory }; - return { - ...project, - taskCategories: { - ...project.taskCategories, - [taskCategory.id]: taskCategory, - } - }; -} - export interface RemoveTaskCategory extends Action { taskCategoryId: TaskCategoryID } @@ -323,18 +328,28 @@ export function removeTaskCategory(taskCategoryId: TaskCategoryID): RemoveTaskCa return { type: REMOVE_TASK_CATEGORY, taskCategoryId }; } -export function handleRemoveTaskCategory(project: Project, action: RemoveTaskCategory): Project { - const taskCategories = { ...project.taskCategories }; - delete taskCategories[action.taskCategoryId]; +export interface TaskCategoryRemoved extends Action { + id: number +} + +export const TASK_CATEGORY_REMOVED = "TASK_CATEGORY_REMOVED"; + +export function taskCategoryRemoved(id: number): TaskCategoryRemoved { + return { type: TASK_CATEGORY_REMOVED, id }; +} + +export function handleTaskCategoryRemoved(project: ProjectState, action: TaskCategoryRemoved): ProjectState { + const taskCategories = [...project.taskCategories]; + const taskCategoryIndex = project.taskCategories.findIndex(tc => tc.id === action.id); + if (taskCategoryIndex === -1) return project; + taskCategories.splice(taskCategoryIndex, 1); return { ...project, - taskCategories: { - ...project.taskCategories, - ...taskCategories - } + taskCategories }; } + export interface ResetProject extends Action { project: Project } @@ -355,9 +370,26 @@ export function resetProject(project: Project): ResetProject { return { type: RESET_PROJECT, project: newProject }; } -export function handleResetProject(project: Project, action: ResetProject): Project { +export function handleResetProject(project: ProjectState, action: ResetProject): ProjectState { return { ...project, ...action.project, }; } + +export interface BackendError extends Action { + err: Error +} + +export const BACKEND_ERROR = "BACKEND_ERROR"; + +export function backendError(err: Error): BackendError { + return { type: BACKEND_ERROR, err }; +} + +export function handleBackendError(project: ProjectState, action: BackendError): ProjectState { + return { + ...project, + lastBackendError: action.err, + }; +} diff --git a/internal/graph/error.go b/internal/graph/error.go new file mode 100644 index 0000000..58e1c37 --- /dev/null +++ b/internal/graph/error.go @@ -0,0 +1,14 @@ +package graph + +import ( + "github.com/vektah/gqlparser/v2/gqlerror" +) + +var ( + ErrAssociatedTaskExist = &gqlerror.Error{ + Message: "", + Extensions: map[string]interface{}{ + "code": "associated-task-exist", + }, + } +) diff --git a/internal/graph/mutation.graphql b/internal/graph/mutation.graphql index 24ee278..19a3797 100644 --- a/internal/graph/mutation.graphql +++ b/internal/graph/mutation.graphql @@ -30,6 +30,11 @@ input TimeUnitChanges { acronym: String } +input ProjectTaskCategoryChanges { + label: String + costPerTimeUnit: Float +} + type Mutation { updateUser(id: ID!, changes: UserChanges!): User! createProject(changes: CreateProjectChanges!): Project! @@ -37,5 +42,8 @@ type Mutation { addProjectTask(projectId: ID!, changes: ProjectTaskChanges!): Task! removeProjectTask(projectId: ID!, taskId: ID!): Boolean! updateProjectTask(projectId: ID!, taskId: ID!, changes: ProjectTaskChanges!): Task! + addProjectTaskCategory(projectId: ID!, changes: ProjectTaskCategoryChanges!): TaskCategory! + updateProjectTaskCategory(projectId: ID!, taskCategoryId: ID!, changes: ProjectTaskCategoryChanges!): TaskCategory! + removeProjectTaskCategory(projectId: ID!, taskCategoryId: ID!): Boolean! updateProjectParams(projectId: ID!, changes: ProjectParamsChanges!): ProjectParams! } \ No newline at end of file diff --git a/internal/graph/mutation.resolvers.go b/internal/graph/mutation.resolvers.go index 87ea0f6..3218d8a 100644 --- a/internal/graph/mutation.resolvers.go +++ b/internal/graph/mutation.resolvers.go @@ -34,6 +34,18 @@ func (r *mutationResolver) UpdateProjectTask(ctx context.Context, projectID int6 return handleUpdateProjectTask(ctx, projectID, taskID, changes) } +func (r *mutationResolver) AddProjectTaskCategory(ctx context.Context, projectID int64, changes model.ProjectTaskCategoryChanges) (*model.TaskCategory, error) { + return handleAddProjectTaskCategory(ctx, projectID, changes) +} + +func (r *mutationResolver) UpdateProjectTaskCategory(ctx context.Context, projectID int64, taskCategoryID int64, changes model.ProjectTaskCategoryChanges) (*model.TaskCategory, error) { + return handleUpdateProjectTaskCategory(ctx, projectID, taskCategoryID, changes) +} + +func (r *mutationResolver) RemoveProjectTaskCategory(ctx context.Context, projectID int64, taskCategoryID int64) (bool, error) { + return handleRemoveProjectTaskCategory(ctx, projectID, taskCategoryID) +} + func (r *mutationResolver) UpdateProjectParams(ctx context.Context, projectID int64, changes model.ProjectParamsChanges) (*model.ProjectParams, error) { return handleUpdateProjectParams(ctx, projectID, changes) } diff --git a/internal/graph/project_handler.go b/internal/graph/project_handler.go index 2743cad..ac16161 100644 --- a/internal/graph/project_handler.go +++ b/internal/graph/project_handler.go @@ -3,6 +3,8 @@ package graph import ( "context" + "github.com/99designs/gqlgen/graphql" + "forge.cadoles.com/Cadoles/guesstimate/internal/model" model1 "forge.cadoles.com/Cadoles/guesstimate/internal/model" "github.com/pkg/errors" @@ -126,3 +128,56 @@ func handleUpdateProjectParams(ctx context.Context, projectID int64, changes mod return project.Params, nil } + +func handleAddProjectTaskCategory(ctx context.Context, projectID int64, changes model.ProjectTaskCategoryChanges) (*model.TaskCategory, error) { + db, err := getDB(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + repo := model.NewProjectRepository(db) + + taskCategory, err := repo.AddTaskCategory(ctx, projectID, changes) + if err != nil { + return nil, errors.WithStack(err) + } + + return taskCategory, nil +} + +func handleUpdateProjectTaskCategory(ctx context.Context, projectID int64, taskCategoryID int64, changes model.ProjectTaskCategoryChanges) (*model.TaskCategory, error) { + db, err := getDB(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + repo := model.NewProjectRepository(db) + + taskCategory, err := repo.UpdateTaskCategory(ctx, projectID, taskCategoryID, changes) + if err != nil { + return nil, errors.WithStack(err) + } + + return taskCategory, nil +} + +func handleRemoveProjectTaskCategory(ctx context.Context, projectID int64, taskCategoryID int64) (bool, error) { + db, err := getDB(ctx) + if err != nil { + return false, errors.WithStack(err) + } + + repo := model.NewProjectRepository(db) + + if err := repo.RemoveTaskCategory(ctx, projectID, taskCategoryID); err != nil { + if errors.Is(err, model.ErrAssociatedTaskExist) { + graphql.AddError(ctx, ErrAssociatedTaskExist) + + return false, nil + } + + return false, errors.WithStack(err) + } + + return true, nil +} diff --git a/internal/model/error.go b/internal/model/error.go new file mode 100644 index 0000000..0595715 --- /dev/null +++ b/internal/model/error.go @@ -0,0 +1,7 @@ +package model + +import "errors" + +var ( + ErrAssociatedTaskExist = errors.New("associated task exist") +) diff --git a/internal/model/project_repository.go b/internal/model/project_repository.go index 569a133..6adb2e5 100644 --- a/internal/model/project_repository.go +++ b/internal/model/project_repository.go @@ -144,10 +144,6 @@ func (r *ProjectRepository) AddTask(ctx context.Context, projectID int64, change return errors.WithStack(err) } - if err := tx.Save(task).Error; err != nil { - return errors.Wrap(err, "could not create task") - } - err := tx.Model(project).Association("Tasks").Append(task).Error if err != nil { return errors.Wrap(err, "could not add task") @@ -279,6 +275,94 @@ func (r *ProjectRepository) UpdateParams(ctx context.Context, projectID int64, c return project, nil } +func (r *ProjectRepository) AddTaskCategory(ctx context.Context, projectID int64, changes ProjectTaskCategoryChanges) (*TaskCategory, error) { + project := &Project{} + project.ID = projectID + taskCategory := &TaskCategory{} + + err := r.db.Transaction(func(tx *gorm.DB) error { + if err := updateTaskCategoryWithChanges(tx, taskCategory, changes); err != nil { + return errors.WithStack(err) + } + + err := tx.Model(project).Association("TaskCategories").Append(taskCategory).Error + if err != nil { + return errors.Wrap(err, "could not add task category") + } + + return nil + }) + if err != nil { + return nil, errors.Wrap(err, "could not add task category") + } + + return taskCategory, nil +} + +func (r *ProjectRepository) RemoveTaskCategory(ctx context.Context, projectID int64, taskCategoryID int64) error { + project := &Project{} + project.ID = projectID + + err := r.db.Transaction(func(tx *gorm.DB) error { + taskCategory := &TaskCategory{} + taskCategory.ID = taskCategoryID + + var totalAssociatedTasks int + if err := tx.Model(&Task{}).Where("category_id = ?", taskCategoryID).Count(&totalAssociatedTasks).Error; err != nil { + return errors.Wrap(err, "could not count associated tasks") + } + + if totalAssociatedTasks != 0 { + return errors.WithStack(ErrAssociatedTaskExist) + } + + err := tx.Model(project).Association("TaskCategories").Delete(taskCategory).Error + if err != nil { + return errors.Wrap(err, "could not remove task category relationship") + } + + err = tx.Delete(taskCategory, "id = ? AND project_id = ?", taskCategoryID, projectID).Error + if err != nil { + return errors.Wrap(err, "could not delete task category") + } + + return nil + }) + if err != nil { + return errors.Wrap(err, "could not remove task category") + } + + return nil +} + +func (r *ProjectRepository) UpdateTaskCategory(ctx context.Context, projectID, taskCategoryID int64, changes ProjectTaskCategoryChanges) (*TaskCategory, error) { + taskCategory := &TaskCategory{} + + err := r.db.Transaction(func(tx *gorm.DB) error { + err := tx.Model(taskCategory). + First(taskCategory, "id = ? AND project_id = ?", taskCategoryID, projectID). + Error + if err != nil { + return errors.WithStack(err) + } + + if err := updateTaskCategoryWithChanges(tx, taskCategory, changes); err != nil { + return errors.WithStack(err) + } + + if err := tx.Save(taskCategory).Error; err != nil { + return errors.WithStack(err) + } + + return nil + }) + if err != nil { + return nil, errors.Wrap(err, "could not update task category") + } + + return taskCategory, nil +} + func updateTaskWithChanges(db *gorm.DB, task *Task, changes ProjectTaskChanges) error { if changes.Label != nil { task.Label = changes.Label @@ -330,6 +414,18 @@ func updateTaskWithChanges(db *gorm.DB, task *Task, changes ProjectTaskChanges) return nil } +func updateTaskCategoryWithChanges(db *gorm.DB, taskCategory *TaskCategory, changes ProjectTaskCategoryChanges) error { + if changes.Label != nil { + taskCategory.Label = *changes.Label + } + + if changes.CostPerTimeUnit != nil { + taskCategory.CostPerTimeUnit = *changes.CostPerTimeUnit + } + + return nil +} + func NewProjectRepository(db *gorm.DB) *ProjectRepository { return &ProjectRepository{db} }