mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-09 13:58:25 +02:00
add comment to company
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
+27
-19
@@ -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)
|
||||
|
||||
@@ -1078,11 +1078,13 @@ export class API {
|
||||
* Create a new company.
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {string} comment
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
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<ApiResponse>}
|
||||
*/
|
||||
update: async (id, name) => {
|
||||
update: async (id, name, comment) => {
|
||||
return await postJSON(this.getPath(`/company/${id}`), {
|
||||
name: name
|
||||
name: name,
|
||||
comment: comment
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
<TableCellAction>
|
||||
<TableDropDownEllipsis>
|
||||
<TableUpdateButton on:click={() => openUpdateModal(company.id)} />
|
||||
<TableDropDownButton
|
||||
name="View Comment"
|
||||
on:click={() => openViewCommentModal(company)}
|
||||
/>
|
||||
<TableDropDownButton name="Export" on:click={() => onClickExport(company.id)} />
|
||||
<TableDeleteButton on:click={() => openDeleteAlert(company)} />
|
||||
</TableDropDownEllipsis>
|
||||
@@ -264,21 +291,94 @@
|
||||
</Table>
|
||||
|
||||
<Modal headerText={modalText} visible={isModalVisible} onClose={closeModal} {isSubmitting}>
|
||||
<FormGrid on:submit={onSubmit} bind:bindTo={form} {isSubmitting}>
|
||||
<FormColumns>
|
||||
<FormColumn>
|
||||
<TextField
|
||||
required
|
||||
minLength={1}
|
||||
maxLength={64}
|
||||
placeholder="Alices Enterprise Solutions"
|
||||
bind:value={formValues.name}>Name</TextField
|
||||
>
|
||||
</FormColumn>
|
||||
</FormColumns>
|
||||
<FormError message={modalError} />
|
||||
<FormFooter {closeModal} {isSubmitting} />
|
||||
</FormGrid>
|
||||
<div class="w-[1000px] p-6">
|
||||
<form on:submit|preventDefault={onSubmit} bind:this={form}>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label
|
||||
for="company-name"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||
>
|
||||
Company Name
|
||||
</label>
|
||||
<input
|
||||
id="company-name"
|
||||
type="text"
|
||||
required
|
||||
minlength="1"
|
||||
maxlength="64"
|
||||
placeholder="Alices Enterprise Solutions"
|
||||
bind:value={formValues.name}
|
||||
class="w-96 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center mb-2">
|
||||
<label
|
||||
for="company-comment"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Comment
|
||||
</label>
|
||||
<div
|
||||
class="bg-gray-100 dark:bg-gray-800/60 ml-2 px-2 rounded-md transition-colors duration-200 h-6 flex items-center"
|
||||
>
|
||||
<p class="text-slate-600 dark:text-gray-400 text-xs transition-colors duration-200">
|
||||
optional
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
id="company-comment"
|
||||
bind:value={formValues.comment}
|
||||
maxlength={1000000}
|
||||
rows="20"
|
||||
placeholder="Add notes about this company..."
|
||||
class="w-full p-4 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormError message={modalError} />
|
||||
<FormFooter {closeModal} {isSubmitting} />
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
headerText="Company Comment"
|
||||
visible={isViewCommentModalVisible}
|
||||
onClose={closeViewCommentModal}
|
||||
>
|
||||
<div class="p-8 w-full min-w-[800px] max-w-6xl">
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2">
|
||||
{viewCommentCompany?.name || 'Company'}
|
||||
</h3>
|
||||
</div>
|
||||
{#if viewCommentCompany?.comment && viewCommentCompany.comment.trim()}
|
||||
<div
|
||||
class="bg-gray-50 dark:bg-gray-800 p-8 rounded-lg border min-h-[400px] max-h-[600px] overflow-y-auto"
|
||||
>
|
||||
<pre
|
||||
class="whitespace-pre-wrap text-base text-gray-700 dark:text-gray-300 font-normal leading-relaxed">{viewCommentCompany.comment}</pre>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="bg-gray-50 dark:bg-gray-800 p-8 rounded-lg border text-center">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 italic">No comment available.</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
on:click={closeViewCommentModal}
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-md transition-colors duration-200"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<DeleteAlert
|
||||
|
||||
Reference in New Issue
Block a user