@@ -7,6 +7,8 @@ import (
|
|||||||
"github.com/gissleh/stufflog/internal/slerrors"
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
"github.com/gissleh/stufflog/models"
|
"github.com/gissleh/stufflog/models"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type projectRepository struct {
|
type projectRepository struct {
|
||||||
@@ -14,17 +16,17 @@ type projectRepository struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *projectRepository) Find(ctx context.Context, id string) (*models.Project, error) {
|
func (r *projectRepository) Find(ctx context.Context, id string) (*models.Project, error) {
|
||||||
res := models.Project{}
|
res := projectDBO{}
|
||||||
err := r.db.GetContext(ctx, &res, "SELECT * FROM project WHERE project_id=$1", id)
|
err := r.db.GetContext(ctx, &res, "SELECT * FROM project WHERE project_id=$1", id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, slerrors.NotFound("Log")
|
return nil, slerrors.NotFound("Project")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &res, nil
|
return res.ToProject(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *projectRepository) List(ctx context.Context, filter models.ProjectFilter) ([]*models.Project, error) {
|
func (r *projectRepository) List(ctx context.Context, filter models.ProjectFilter) ([]*models.Project, error) {
|
||||||
@@ -60,26 +62,35 @@ func (r *projectRepository) List(ctx context.Context, filter models.ProjectFilte
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res := make([]*models.Project, 0, 8)
|
res := make([]*projectDBO, 0, 8)
|
||||||
err = r.db.SelectContext(ctx, &res, query, args...)
|
err = r.db.SelectContext(ctx, &res, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return res, nil
|
return []*models.Project{}, nil
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
res2 := make([]*models.Project, len(res))
|
||||||
|
for i, v := range res {
|
||||||
|
res2[i] = v.ToProject()
|
||||||
|
}
|
||||||
|
|
||||||
|
return res2, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *projectRepository) Insert(ctx context.Context, project models.Project) error {
|
func (r *projectRepository) Insert(ctx context.Context, project models.Project) error {
|
||||||
|
if project.Tags == nil {
|
||||||
|
project.Tags = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
_, err := r.db.NamedExecContext(ctx, `
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
INSERT INTO project(
|
INSERT INTO project(
|
||||||
project_id, user_id, name, description, icon, active, created_time, start_time, end_time, subtract_amount, status_tag, favorite
|
project_id, user_id, name, description, icon, active, created_time, start_time, end_time, subtract_amount, status_tag, favorite, tags
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:project_id, :user_id, :name, :description, :icon, :active, :created_time, :start_time, :end_time, :subtract_amount, :status_tag, :favorite
|
:project_id, :user_id, :name, :description, :icon, :active, :created_time, :start_time, :end_time, :subtract_amount, :status_tag, :favorite, :tags
|
||||||
)
|
)
|
||||||
`, &project)
|
`, toProjectDBO(project))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -88,6 +99,10 @@ func (r *projectRepository) Insert(ctx context.Context, project models.Project)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *projectRepository) Update(ctx context.Context, project models.Project) error {
|
func (r *projectRepository) Update(ctx context.Context, project models.Project) error {
|
||||||
|
if project.Tags == nil {
|
||||||
|
project.Tags = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
_, err := r.db.NamedExecContext(ctx, `
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
UPDATE project SET
|
UPDATE project SET
|
||||||
name = :name,
|
name = :name,
|
||||||
@@ -98,9 +113,10 @@ func (r *projectRepository) Update(ctx context.Context, project models.Project)
|
|||||||
end_time = :end_time,
|
end_time = :end_time,
|
||||||
subtract_amount = :subtract_amount,
|
subtract_amount = :subtract_amount,
|
||||||
status_tag = :status_tag,
|
status_tag = :status_tag,
|
||||||
favorite = :favorite
|
favorite = :favorite,
|
||||||
|
tags = :tags
|
||||||
WHERE project_id=:project_id
|
WHERE project_id=:project_id
|
||||||
`, &project)
|
`, toProjectDBO(project))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -124,3 +140,55 @@ func (r *projectRepository) Delete(ctx context.Context, project models.Project)
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type projectDBO struct {
|
||||||
|
ID string `json:"id" db:"project_id"`
|
||||||
|
UserID string `json:"-" db:"user_id"`
|
||||||
|
Name string `json:"name" db:"name"`
|
||||||
|
Description string `json:"description" db:"description"`
|
||||||
|
Icon string `json:"icon" db:"icon"`
|
||||||
|
Active bool `json:"active" db:"active"`
|
||||||
|
CreatedTime time.Time `json:"createdTime" db:"created_time"`
|
||||||
|
StartTime *time.Time `json:"startTime" db:"start_time"`
|
||||||
|
EndTime *time.Time `json:"endTime" db:"end_time"`
|
||||||
|
SubtractAmount int `json:"subtractAmount" db:"subtract_amount"`
|
||||||
|
StatusTag *string `json:"statusTag" db:"status_tag"`
|
||||||
|
Favorite bool `json:"favorite" db:"favorite"`
|
||||||
|
Tags pq.StringArray `json:"tags" db:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toProjectDBO(project models.Project) *projectDBO {
|
||||||
|
return &projectDBO{
|
||||||
|
ID: project.ID,
|
||||||
|
UserID: project.UserID,
|
||||||
|
Name: project.Name,
|
||||||
|
Description: project.Description,
|
||||||
|
Icon: project.Icon,
|
||||||
|
Active: project.Active,
|
||||||
|
CreatedTime: project.CreatedTime,
|
||||||
|
StartTime: project.StartTime,
|
||||||
|
EndTime: project.EndTime,
|
||||||
|
SubtractAmount: project.SubtractAmount,
|
||||||
|
StatusTag: project.StatusTag,
|
||||||
|
Favorite: project.Favorite,
|
||||||
|
Tags: project.Tags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *projectDBO) ToProject() *models.Project {
|
||||||
|
return &models.Project{
|
||||||
|
ID: d.ID,
|
||||||
|
UserID: d.UserID,
|
||||||
|
Name: d.Name,
|
||||||
|
Description: d.Description,
|
||||||
|
Icon: d.Icon,
|
||||||
|
Active: d.Active,
|
||||||
|
CreatedTime: d.CreatedTime,
|
||||||
|
StartTime: d.StartTime,
|
||||||
|
EndTime: d.EndTime,
|
||||||
|
SubtractAmount: d.SubtractAmount,
|
||||||
|
StatusTag: d.StatusTag,
|
||||||
|
Favorite: d.Favorite,
|
||||||
|
Tags: d.Tags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE project
|
||||||
|
ADD COLUMN tags TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE project
|
||||||
|
DROP COLUMN tags;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -18,6 +18,7 @@ type Project struct {
|
|||||||
SubtractAmount int `json:"subtractAmount" db:"subtract_amount"`
|
SubtractAmount int `json:"subtractAmount" db:"subtract_amount"`
|
||||||
StatusTag *string `json:"statusTag" db:"status_tag"`
|
StatusTag *string `json:"statusTag" db:"status_tag"`
|
||||||
Favorite bool `json:"favorite" db:"favorite"`
|
Favorite bool `json:"favorite" db:"favorite"`
|
||||||
|
Tags []string `json:"tags" db:"tags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (project *Project) Update(update ProjectUpdate) {
|
func (project *Project) Update(update ProjectUpdate) {
|
||||||
@@ -62,6 +63,9 @@ func (project *Project) Update(update ProjectUpdate) {
|
|||||||
if update.Favorite != nil {
|
if update.Favorite != nil {
|
||||||
project.Favorite = *update.Favorite
|
project.Favorite = *update.Favorite
|
||||||
}
|
}
|
||||||
|
if update.SetTags != nil {
|
||||||
|
project.Tags = update.SetTags
|
||||||
|
}
|
||||||
|
|
||||||
if project.StatusTag != nil && project.Active {
|
if project.StatusTag != nil && project.Active {
|
||||||
project.StatusTag = nil
|
project.StatusTag = nil
|
||||||
@@ -80,6 +84,7 @@ type ProjectUpdate struct {
|
|||||||
SubtractAmount *int `json:"subtractAmount"`
|
SubtractAmount *int `json:"subtractAmount"`
|
||||||
StatusTag *string `json:"statusTag"`
|
StatusTag *string `json:"statusTag"`
|
||||||
ClearStatusTag bool `json:"clearStatusTag"`
|
ClearStatusTag bool `json:"clearStatusTag"`
|
||||||
|
SetTags []string `json:"setTags"`
|
||||||
Favorite *bool `json:"favorite"`
|
Favorite *bool `json:"favorite"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type ProjectGroup struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
UserID string `json:"-" db:"user_id"`
|
||||||
|
Name string `json:"name" db:"name"`
|
||||||
|
Abbreviation string `json:"abbreviation" db:"abbreviation"`
|
||||||
|
CategoryNames map[string]string `json:"categoryNames" db:"category_names"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectGroupUpdate struct {
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Abbreviation *string `json:"abbreviation"`
|
||||||
|
SetCategoryNames map[string]string `json:"setCategoryNames"`
|
||||||
|
ResetCategoryName *string `json:"resetCategoryName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectGroupFilter struct {
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectGroupRepository interface {
|
||||||
|
Find(ctx context.Context, id string) (*ProjectGroup, error)
|
||||||
|
List(ctx context.Context, filter ProjectGroupFilter) ([]*ProjectGroup, error)
|
||||||
|
Insert(ctx context.Context, group ProjectGroup) error
|
||||||
|
Update(ctx context.Context, group ProjectGroup) error
|
||||||
|
Delete(ctx context.Context, group ProjectGroup) error
|
||||||
|
}
|
||||||
@@ -92,6 +92,7 @@ import TimeProgress from "./TimeProgress.svelte";
|
|||||||
<TimeProgress startTime={entry.startTime || entry.createdTime} endTime={entry.endTime} />
|
<TimeProgress startTime={entry.startTime || entry.createdTime} endTime={entry.endTime} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
<slot name="above-description"></slot>
|
||||||
{#if (full)}
|
{#if (full)}
|
||||||
<Markdown source={entry.description} />
|
<Markdown source={entry.description} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import stuffLogClient from "../clients/stufflog";
|
|||||||
import ParentEntry from "./ParentEntry.svelte";
|
import ParentEntry from "./ParentEntry.svelte";
|
||||||
import ProgressNumbers from "./ProgressNumbers.svelte";
|
import ProgressNumbers from "./ProgressNumbers.svelte";
|
||||||
import StatusColor from "./StatusColor.svelte";
|
import StatusColor from "./StatusColor.svelte";
|
||||||
|
import TagList from "./TagList.svelte";
|
||||||
import TaskList from "./TaskList.svelte";
|
import TaskList from "./TaskList.svelte";
|
||||||
|
|
||||||
export let project: ProjectResult = null;
|
export let project: ProjectResult = null;
|
||||||
@@ -113,6 +114,9 @@ import stuffLogClient from "../clients/stufflog";
|
|||||||
<div slot="post-seprator">
|
<div slot="post-seprator">
|
||||||
<ProgressNumbers project={project} />
|
<ProgressNumbers project={project} />
|
||||||
</div>
|
</div>
|
||||||
|
<div slot="above-description">
|
||||||
|
<TagList tags={project.tags} />
|
||||||
|
</div>
|
||||||
{#if showAllOptions}
|
{#if showAllOptions}
|
||||||
<OptionRow>
|
<OptionRow>
|
||||||
<Option open={mdAddTask}>Add Task</Option>
|
<Option open={mdAddTask}>Add Task</Option>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ProjectResult } from "../models/project";
|
import App from "../App.svelte";
|
||||||
|
import type { ProjectResult } from "../models/project";
|
||||||
import selectionStore from "../stores/selection";
|
import selectionStore from "../stores/selection";
|
||||||
import DaysLeft from "./DaysLeft.svelte";
|
import DaysLeft from "./DaysLeft.svelte";
|
||||||
import Icon from "./Icon.svelte";
|
import Icon from "./Icon.svelte";
|
||||||
import ProjectIcon from "./ProjectIcon.svelte";
|
import ProjectIcon from "./ProjectIcon.svelte";
|
||||||
import ProjectProgress from "./ProjectProgress.svelte";
|
import ProjectProgress from "./ProjectProgress.svelte";
|
||||||
import StatusColor from "./StatusColor.svelte";
|
import StatusColor from "./StatusColor.svelte";
|
||||||
|
import Tag from "./Tag.svelte";
|
||||||
import TimeProgress from "./TimeProgress.svelte";
|
import TimeProgress from "./TimeProgress.svelte";
|
||||||
|
|
||||||
export let project: ProjectResult;
|
export let project: ProjectResult;
|
||||||
@@ -30,6 +32,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="name">
|
<div class="name">
|
||||||
|
{#if project.tags.length > 0}
|
||||||
|
<Tag small value={project.tags[0]} />
|
||||||
|
{/if}
|
||||||
<div class="content">{project.name}</div>
|
<div class="content">{project.name}</div>
|
||||||
{#if project.endTime}
|
{#if project.endTime}
|
||||||
<div class="times">
|
<div class="times">
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let value = "";
|
||||||
|
export let small = false;
|
||||||
|
|
||||||
|
let style = "";
|
||||||
|
|
||||||
|
$: {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
hash = value.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
hash = hash & hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(value, hash);
|
||||||
|
|
||||||
|
style = `color: hsl(${(hash << 4) % 360}, ${40 + (hash % 8)}%, ${50+(hash % 4)}%)`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style={style} class="tag" class:small>{value}</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.tag {
|
||||||
|
padding: 0.25em 0.75ch;
|
||||||
|
margin: 0.25em 0.5ch;
|
||||||
|
font-size: 0.75em;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.tag.small {
|
||||||
|
padding: 0em 0.5ch;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Tag from "./Tag.svelte";
|
||||||
|
|
||||||
|
export let tags: string[] = [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="tag-list">
|
||||||
|
{#each tags as tag (tag)}
|
||||||
|
<Tag value={tag} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.tag-list {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 0.15em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
tasks: [],
|
tasks: [],
|
||||||
favorite: false,
|
favorite: false,
|
||||||
subtractAmount: 0,
|
subtractAmount: 0,
|
||||||
|
tags: [],
|
||||||
}
|
}
|
||||||
let verb = "Add";
|
let verb = "Add";
|
||||||
if (md.name === "project.edit" || md.name === "project.delete") {
|
if (md.name === "project.edit" || md.name === "project.delete") {
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
let subtractAmount = project.subtractAmount;
|
let subtractAmount = project.subtractAmount;
|
||||||
let error = null;
|
let error = null;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
let tags = project.tags.join(", ");
|
||||||
|
|
||||||
function onSubmit() {
|
function onSubmit() {
|
||||||
loading = true;
|
loading = true;
|
||||||
@@ -60,6 +62,7 @@
|
|||||||
endTime: ( endTime == "" ) ? null : new Date(endTime),
|
endTime: ( endTime == "" ) ? null : new Date(endTime),
|
||||||
statusTag: statusTag !== "" ? statusTag : null,
|
statusTag: statusTag !== "" ? statusTag : null,
|
||||||
subtractAmount: Math.min(subtractAmount, 0),
|
subtractAmount: Math.min(subtractAmount, 0),
|
||||||
|
tags: tags.length > 0 ? tags.split(",").map(t => t.trim()) : [],
|
||||||
|
|
||||||
name, description, icon, favorite,
|
name, description, icon, favorite,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
@@ -89,6 +92,7 @@
|
|||||||
statusTag: statusTag || null,
|
statusTag: statusTag || null,
|
||||||
clearStatusTag: statusTag === "",
|
clearStatusTag: statusTag === "",
|
||||||
subtractAmount: subtractAmount,
|
subtractAmount: subtractAmount,
|
||||||
|
setTags: tags.length > 0 ? tags.split(",").map(t => t.trim()) : [],
|
||||||
|
|
||||||
name, description, icon, favorite,
|
name, description, icon, favorite,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
@@ -128,6 +132,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<label for="statusTag">Status</label>
|
<label for="statusTag">Status</label>
|
||||||
<StatusTagSelect disabled={deletion} isProject bind:value={statusTag} />
|
<StatusTagSelect disabled={deletion} isProject bind:value={statusTag} />
|
||||||
|
<label for="tags">Tags (Comma Separated)</label>
|
||||||
|
<input disabled={deletion} name="tags" type="text" bind:value={tags} />
|
||||||
|
|
||||||
<Checkbox disabled={deletion} bind:checked={favorite} label="Mark as favorite." />
|
<Checkbox disabled={deletion} bind:checked={favorite} label="Mark as favorite." />
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export default interface Project {
|
|||||||
createdTime: string
|
createdTime: string
|
||||||
favorite: boolean
|
favorite: boolean
|
||||||
subtractAmount: number
|
subtractAmount: number
|
||||||
|
tags: string[]
|
||||||
startTime?: string
|
startTime?: string
|
||||||
endTime?: string
|
endTime?: string
|
||||||
statusTag?: string
|
statusTag?: string
|
||||||
@@ -36,6 +37,7 @@ export interface ProjectInput {
|
|||||||
endTime?: string | Date
|
endTime?: string | Date
|
||||||
statusTag?: string
|
statusTag?: string
|
||||||
favorite?: boolean
|
favorite?: boolean
|
||||||
|
tags?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectUpdate {
|
export interface ProjectUpdate {
|
||||||
@@ -51,4 +53,5 @@ export interface ProjectUpdate {
|
|||||||
clearStatusTag?: boolean
|
clearStatusTag?: boolean
|
||||||
subtractAmount?: number
|
subtractAmount?: number
|
||||||
favorite?: boolean
|
favorite?: boolean
|
||||||
|
setTags?: string[]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user