From a6374bd976ff6f099e2a9fc6b4de6f68849e3103 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Tue, 14 Oct 2025 18:19:52 +0200 Subject: [PATCH] add comment to company Signed-off-by: Ronni Skansing --- backend/controller/company.go | 3 + backend/database/company.go | 1 + backend/model/company.go | 15 ++- backend/repository/company.go | 6 + backend/service/company.go | 46 +++++--- frontend/src/lib/api/api.js | 12 +- frontend/src/routes/company/+page.svelte | 144 +++++++++++++++++++---- 7 files changed, 178 insertions(+), 49 deletions(-) diff --git a/backend/controller/company.go b/backend/controller/company.go index a30f38c..37d3fa4 100644 --- a/backend/controller/company.go +++ b/backend/controller/company.go @@ -26,6 +26,7 @@ var CompanyColumnsMap = map[string]string{ "created_at": repository.TableColumn(database.COMPANY_TABLE, "created_at"), "updated_at": repository.TableColumn(database.COMPANY_TABLE, "updated_at"), "name": repository.TableColumn(database.COMPANY_TABLE, "name"), + "comment": repository.TableColumn(database.COMPANY_TABLE, "comment"), } // Company is a Company controller @@ -93,6 +94,7 @@ func (c *Company) ExportByCompanyID(g *gin.Context) { "Created at", "Updated at", "Name", + "Comment", } err = writer.Write(headers) if ok := c.handleErrors(g, err); !ok { @@ -102,6 +104,7 @@ func (c *Company) ExportByCompanyID(g *gin.Context) { utils.CSVFromDate(company.CreatedAt), utils.CSVFromDate(company.UpdatedAt), utils.CSVRemoveFormulaStart(utils.NullableToString(company.Name)), + utils.CSVRemoveFormulaStart(utils.NullableToString(company.Comment)), } err = writer.Write(row) if ok := c.handleErrors(g, err); !ok { diff --git a/backend/database/company.go b/backend/database/company.go index efde0e7..207784c 100644 --- a/backend/database/company.go +++ b/backend/database/company.go @@ -15,6 +15,7 @@ type Company struct { CreatedAt *time.Time `gorm:"not null;index;"` UpdatedAt *time.Time `gorm:"not null;index"` Name string `gorm:"not null;unique;index"` + Comment *string `gorm:"type:text"` // backref: many-to-one Users []*User //`gorm:"foreignKey:CompanyID;"` diff --git a/backend/model/company.go b/backend/model/company.go index a142713..fbf7d8e 100644 --- a/backend/model/company.go +++ b/backend/model/company.go @@ -11,10 +11,11 @@ import ( // Company is a company type Company struct { - ID nullable.Nullable[uuid.UUID] `json:"id"` - CreatedAt *time.Time `json:"createdAt"` - UpdatedAt *time.Time `json:"updatedAt"` - Name nullable.Nullable[vo.String64] `json:"name"` + ID nullable.Nullable[uuid.UUID] `json:"id"` + CreatedAt *time.Time `json:"createdAt"` + UpdatedAt *time.Time `json:"updatedAt"` + Name nullable.Nullable[vo.String64] `json:"name"` + Comment nullable.Nullable[vo.OptionalString1MB] `json:"comment"` } // Validate checks if the Company configuration with a valid state @@ -36,5 +37,11 @@ func (c *Company) ToDBMap() map[string]any { m["name"] = name.String() } } + if c.Comment.IsSpecified() { + m["comment"] = nil + if comment, err := c.Comment.Get(); err == nil { + m["comment"] = comment.String() + } + } return m } diff --git a/backend/repository/company.go b/backend/repository/company.go index 2a65d1f..9e6eb52 100644 --- a/backend/repository/company.go +++ b/backend/repository/company.go @@ -17,6 +17,7 @@ var companyAllowedColumns = assignTableToColumns(database.COMPANY_TABLE, []strin "created_at", "updated_at", "name", + "comment", }) // Company is a Company repository @@ -147,10 +148,15 @@ func (r *Company) DeleteByID( func ToCompany(row *database.Company) *model.Company { id := nullable.NewNullableWithValue(row.ID) name := nullable.NewNullableWithValue(*vo.NewString64Must(row.Name)) + var comment nullable.Nullable[vo.OptionalString1MB] + if row.Comment != nil { + comment = nullable.NewNullableWithValue(*vo.NewUnsafeOptionalString1MB(*row.Comment)) + } return &model.Company{ ID: id, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, Name: name, + Comment: comment, } } diff --git a/backend/service/company.go b/backend/service/company.go index bedbffa..53579d4 100644 --- a/backend/service/company.go +++ b/backend/service/company.go @@ -197,31 +197,39 @@ func (s *Company) UpdateByID( s.Logger.Debugw("failed to get company name", "error", err) return err } - // check if company name is unique, let a TOCTOU error - // happen as a generic error, this is easier than checking - // all database types specific unique contraint errors - _, err = s.CompanyRepository.GetByName( - ctx, - name.String(), - ) - // we expect not to find a company with this name - // so any error is an actual error - if err != nil { - // something went wrong - if !errors.Is(err, gorm.ErrRecordNotFound) { - s.Logger.Debugw("failed to get existing company name", "error", current.Name) - return err + // check if company name is unique, excluding the current company + // only check if the name has actually changed + currentName := current.Name.MustGet().String() + if name.String() != currentName { + // check if company name is unique, let a TOCTOU error + // happen as a generic error, this is easier than checking + // all database types specific unique contraint errors + existingCompany, err := s.CompanyRepository.GetByName( + ctx, + name.String(), + ) + // we expect not to find a company with this name + if err != nil { + // something went wrong + if !errors.Is(err, gorm.ErrRecordNotFound) { + s.Logger.Debugw("failed to get existing company name", "error", err) + return err + } + } + // if there is no error, then the company name is already taken + // but we need to make sure it's not the same company we're updating + if err == nil && existingCompany.ID.MustGet() != *id { + s.Logger.Debugw("company name is already taken", "error", name.String()) + return validate.WrapErrorWithField(errors.New("not unique"), "name") } - } - // if there is no error, then the company name is already taken - if err == nil { - s.Logger.Debugw("company name is already taken", "error", name.String()) - return validate.WrapErrorWithField(errors.New("not unique"), "name") } // update changed fields if v, err := company.Name.Get(); err == nil { current.Name.Set(v) } + if v, err := company.Comment.Get(); err == nil { + current.Comment.Set(v) + } // validate if err := company.Validate(); err != nil { s.Logger.Errorw("failed to validate company", "error", err) diff --git a/frontend/src/lib/api/api.js b/frontend/src/lib/api/api.js index a631797..a943469 100644 --- a/frontend/src/lib/api/api.js +++ b/frontend/src/lib/api/api.js @@ -1078,11 +1078,13 @@ export class API { * Create a new company. * * @param {string} name + * @param {string} comment * @returns {Promise} */ - create: async (name) => { + create: async (name, comment) => { return await postJSON(this.getPath(`/company`), { - name: name + name: name, + comment: comment }); }, @@ -1091,11 +1093,13 @@ export class API { * * @param {string} id * @param {string} name + * @param {string} comment * @returns {Promise} */ - update: async (id, name) => { + update: async (id, name, comment) => { return await postJSON(this.getPath(`/company/${id}`), { - name: name + name: name, + comment: comment }); }, diff --git a/frontend/src/routes/company/+page.svelte b/frontend/src/routes/company/+page.svelte index bfe84f4..894aae6 100644 --- a/frontend/src/routes/company/+page.svelte +++ b/frontend/src/routes/company/+page.svelte @@ -2,7 +2,6 @@ import { api } from '$lib/api/apiProxy.js'; import { onMount } from 'svelte'; import Headline from '$lib/components/Headline.svelte'; - import TextField from '$lib/components/TextField.svelte'; import TableRow from '$lib/components/table/TableRow.svelte'; import TableCell from '$lib/components/table/TableCell.svelte'; import TableUpdateButton from '$lib/components/table/TableUpdateButton.svelte'; @@ -13,10 +12,7 @@ import TableCellEmpty from '$lib/components/table/TableCellEmpty.svelte'; import { newTableURLParams } from '$lib/service/tableURLParams.js'; import Modal from '$lib/components/Modal.svelte'; - import FormGrid from '$lib/components/FormGrid.svelte'; import BigButton from '$lib/components/BigButton.svelte'; - import FormColumns from '$lib/components/FormColumns.svelte'; - import FormColumn from '$lib/components/FormColumn.svelte'; import FormFooter from '$lib/components/FormFooter.svelte'; import Table from '$lib/components/table/Table.svelte'; import HeadTitle from '$lib/components/HeadTitle.svelte'; @@ -28,7 +24,8 @@ // bindings let form = null; const formValues = { - name: null + name: null, + comment: null }; // data let modalError = ''; @@ -46,6 +43,9 @@ name: null }; + let isViewCommentModalVisible = false; + let viewCommentCompany = null; + $: { modalText = modalMode === 'create' ? 'New company' : 'Update company'; } @@ -120,7 +120,7 @@ const create = async () => { modalError = ''; try { - const res = await api.company.create(formValues.name); + const res = await api.company.create(formValues.name, formValues.comment); if (!res.success) { modalError = res.error; return; @@ -137,7 +137,7 @@ const update = async () => { modalError = ''; try { - const res = await api.company.update(formValues.id, formValues.name); + const res = await api.company.update(formValues.id, formValues.name, formValues.comment); if (!res.success) { modalError = res.error; return; @@ -179,12 +179,20 @@ const openCreateModal = () => { modalMode = 'create'; modalError = ''; + // reset form values for create mode + formValues.id = null; + formValues.name = null; + formValues.comment = null; isModalVisible = true; }; const closeModal = () => { modalError = ''; isModalVisible = false; + // reset form values + formValues.id = null; + formValues.name = null; + formValues.comment = null; form.reset(); }; @@ -198,6 +206,7 @@ const company = await getCompany(id); formValues.id = company.id; formValues.name = company.name; + formValues.comment = company.comment || null; isModalVisible = true; } catch (e) { addToast('Failed to get company', 'Error'); @@ -210,9 +219,23 @@ const closeUpdateModal = () => { isModalVisible = false; modalError = ''; + // reset form values + formValues.id = null; + formValues.name = null; + formValues.comment = null; form.reset(); }; + const openViewCommentModal = (company) => { + viewCommentCompany = company; + isViewCommentModalVisible = true; + }; + + const closeViewCommentModal = () => { + isViewCommentModalVisible = false; + viewCommentCompany = null; + }; + const onClickExport = async (id) => { try { showIsLoading(); @@ -255,6 +278,10 @@ openUpdateModal(company.id)} /> + openViewCommentModal(company)} + /> onClickExport(company.id)} /> openDeleteAlert(company)} /> @@ -264,21 +291,94 @@ - - - - Name - - - - - +
+
+
+
+ + +
+ +
+
+ +
+

+ optional +

+
+
+