From 4f46967f28c99d3180b32efe90ced252ad68575a Mon Sep 17 00:00:00 2001 From: seb Date: Thu, 6 Aug 2026 03:56:32 +0200 Subject: [PATCH] Add supplier CRUD on entity with Lieferanten group and entityhistory --- README.md | 19 + generate-schema-graph.js | 1 + schema-graph.dot | 144 +- schema-graph.svg | 2605 +++++++++-------- scripts/test-api.js | 86 + src/index.js | 11 + src/schema.js | 26 +- src/sql/customergroup.js | 6 + src/sql/entity.js | 59 + src/sql/entityhistory.js | 11 + src/sql/purge.js | 1 + src/store/companyuser/resolveCompanyAccess.js | 1 + src/store/supplier/create.js | 54 + src/store/supplier/delete.js | 25 + src/store/supplier/ensureLieferantenGroup.js | 26 + src/store/supplier/list.js | 23 + src/store/supplier/update.js | 93 + 17 files changed, 1853 insertions(+), 1338 deletions(-) create mode 100644 src/sql/entity.js create mode 100644 src/sql/entityhistory.js create mode 100644 src/store/supplier/create.js create mode 100644 src/store/supplier/delete.js create mode 100644 src/store/supplier/ensureLieferantenGroup.js create mode 100644 src/store/supplier/list.js create mode 100644 src/store/supplier/update.js diff --git a/README.md b/README.md index 7de4925..c3daa7f 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,21 @@ Company-scoped brands. Auth via `resolveCompanyAccess`. --- +## `supplier` + +Suppliers are `entity` rows in the company `customergroup` named `Lieferanten` (created implicitly on first supplier). Auth via `resolveCompanyAccess`. + +| Method | Signature | Returns | +| --- | --- | --- | +| `list` | `(googleAuthId, companyId)` | `{ id, name, createdAt, updatedAt }[]` | +| `create` | `(googleAuthId, companyId, { name })` | `{ id, name, createdAt, updatedAt }` | +| `update` | `(googleAuthId, companyId, id, { name })` | `{ id, name, createdAt, updatedAt }` | +| `delete` | `(googleAuthId, companyId, id)` | `void` | + +`create` ensures `customergroup` `Lieferanten` for the company, inserts `entity` (`name` only; `first_name` null), writes `entityhistory` (`Erstellt`) with the auth `companyuser_id`. `update` renames and writes `Name geändert von "…" auf "…"` when the name actually changes; bumps `updated_at`. Empty name → `Name required`. `delete` removes the entity (`entityhistory` cascades; `manifest.entity_id` set null). Errors: `Company not found`, `Not authorized`, `Name required`, `Supplier not found`. + +--- + ## `posterminal` Company-scoped POS terminals. Auth via `resolveCompanyAccess`. @@ -460,6 +475,10 @@ res.set('Content-Type', thumb.contentType).send(thumb.buffer); | `GET /api/brand` | `brand.list(googleAuthId, companyId)` | | `PUT /api/brand` | `brand.upsert(googleAuthId, companyId, { name, gsprpinfo? })` | | `DELETE /api/brand/:id` | `brand.delete(googleAuthId, companyId, id)` | +| `GET /api/supplier` | `supplier.list(googleAuthId, companyId)` | +| `POST /api/supplier` | `supplier.create(googleAuthId, companyId, { name })` | +| `PATCH /api/supplier/:id` | `supplier.update(googleAuthId, companyId, id, { name })` | +| `DELETE /api/supplier/:id` | `supplier.delete(googleAuthId, companyId, id)` | | `GET /api/posterminal` | `posterminal.list(googleAuthId, companyId)` | | `PUT /api/posterminal` | `posterminal.upsert(googleAuthId, companyId, { id?, name, token?, pairingpin })` | | `DELETE /api/posterminal/:id` | `posterminal.delete(googleAuthId, companyId, id)` | diff --git a/generate-schema-graph.js b/generate-schema-graph.js index d9edbbc..ff6b20f 100644 --- a/generate-schema-graph.js +++ b/generate-schema-graph.js @@ -274,6 +274,7 @@ const CLUSTERS = [ 'companysetting', 'customergroup', 'entity', + 'entityhistory', 'numbers', ], }, diff --git a/schema-graph.dot b/schema-graph.dot index 1b5d6f3..4221cc3 100644 --- a/schema-graph.dot +++ b/schema-graph.dot @@ -9,24 +9,25 @@ digraph schema { label="Identity & Company"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; - numbers [label=<
numbers
idINT PK
company_idINT FK
typeNVARCHAR(100)
prefixNVARCHAR(50)
paddingINT
nextINT
>]; - entity [label=<
entity
idINT PK
customergroup_idINT FK
firstnameNVARCHAR(255)
lastnameNVARCHAR(255)
>]; - companyuser [label=<
companyuser
idINT PK
company_idINT FK
google_auth_idINT FK
>]; - syncpos [label=<
syncpos
idINT PK
company_idINT FK
paircodeNVARCHAR(50)
token?NVARCHAR(255)
>]; - company [label=<
company
idINT PK
default_language_id?INT FK
root_category_id?INT FK
poscustomergroup_idINT FK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
nameNVARCHAR(255)
>]; + entity [label=<
entity
idINT PK
customergroup_idINT FK
first_name?NVARCHAR(255)
nameNVARCHAR(255)
created_atDATETIME2
updated_atDATETIME2
>]; meta [label=<
meta
keyNVARCHAR(255)
valueNVARCHAR(MAX)
>]; + numbers [label=<
numbers
idINT PK
company_idINT FK
typeNVARCHAR(100)
prefixNVARCHAR(50)
paddingINT
nextINT
>]; customergroup [label=<
customergroup
idINT PK
company_id?INT FK
nameNVARCHAR(255)
>]; + syncpos [label=<
syncpos
idINT PK
company_idINT FK
paircodeNVARCHAR(50)
token?NVARCHAR(255)
>]; + company [label=<
company
idINT PK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
poscustomergroup_idINT FK
root_category_id?INT FK
default_language_id?INT FK
nameNVARCHAR(255)
>]; + entityhistory [label=<
entityhistory
idINT PK
entity_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; companysetting [label=<
companysetting
idINT PK
company_idINT FK
keyNVARCHAR(255)
valueNVARCHAR(MAX)
frontendBIT
>]; + companyuser [label=<
companyuser
idINT PK
company_idINT FK
google_auth_idINT FK
>]; google_auth [label=<
google_auth
idINT PK
selected_company?INT FK
emailNVARCHAR(320)
picture?NVARCHAR(2048)
picturebinary?VARBINARY(MAX)
sub?NVARCHAR(255)
created_atDATETIME2
>]; } subgraph cluster_i18n { label="Localization"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; + unittext [label=<
unittext
idINT PK
unit_idINT FK
language_idINT FK
symbolNVARCHAR(50)
nameNVARCHAR(255)
>]; language [label=<
language
idINT PK
iso639NVARCHAR(10)
>]; - articletext [label=<
articletext
idINT PK
language_idINT FK
article_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; - unittext [label=<
unittext
idINT PK
language_idINT FK
unit_idINT FK
symbolNVARCHAR(50)
nameNVARCHAR(255)
>]; - categorytext [label=<
categorytext
idINT PK
language_idINT FK
category_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; + articletext [label=<
articletext
idINT PK
article_idINT FK
language_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; + categorytext [label=<
categorytext
idINT PK
category_idINT FK
language_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; } subgraph cluster_catalog { label="Catalog"; labeljust="l"; fontsize=15; @@ -34,32 +35,32 @@ digraph schema { fontcolor="#1a365d"; margin=14; category [label=<
category
idINT PK
parent_id?INT FK
sortINT
>]; categoryarticle [label=<
categoryarticle
idINT PK
category_idINT FK
article_idINT FK
>]; - articlelistarticle [label=<
articlelistarticle
idINT PK
article_idINT FK
articlelist_idINT FK
countINT
sortINT
>]; - brand [label=<
brand
idINT PK
company_idINT FK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; - articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; - article [label=<
article
idINT PK
brand_id?INT FK
fatherarticle_id?INT FK
articlelist_id?INT FK
optionslist_id?INT FK
unit_idINT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; + articlelistarticle [label=<
articlelistarticle
idINT PK
articlelist_idINT FK
article_idINT FK
countINT
sortINT
>]; articlelist [label=<
articlelist
idINT PK
>]; - articlehistory [label=<
articlehistory
idINT PK
article_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; + article [label=<
article
idINT PK
unit_idINT FK
articlelist_id?INT FK
fatherarticle_id?INT FK
optionslist_id?INT FK
brand_id?INT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; + articlehistory [label=<
articlehistory
idINT PK
companyuser_id?INT FK
article_idINT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; optionslist [label=<
optionslist
idINT PK
>]; + articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; + brand [label=<
brand
idINT PK
company_idINT FK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; optionslistarticle [label=<
optionslistarticle
idINT PK
article_idINT FK
optionslist_idINT FK
countINT
sortINT
mandatedBIT
>]; } subgraph cluster_media { label="Media"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; - categorypic [label=<
categorypic
idINT PK
pic_idINT FK
category_idINT FK
sortINT
>]; + categorypic [label=<
categorypic
idINT PK
category_idINT FK
pic_idINT FK
sortINT
>]; + articlepic [label=<
articlepic
idINT PK
article_idINT FK
pic_idINT FK
sortINT
>]; pic [label=<
pic
idINT PK
wINT
hINT
sizeBIGINT
hashNVARCHAR(64)
thumbwINT
thumbhINT
thumbhashNVARCHAR(64)
sourceNVARCHAR(MAX)
fullbinaryVARBINARY(MAX)
thumbbinaryVARBINARY(MAX)
>]; - articlepic [label=<
articlepic
idINT PK
pic_idINT FK
article_idINT FK
sortINT
>]; - brandpic [label=<
brandpic
idINT PK
pic_idINT FK
brand_idINT FK
sortINT
>]; + brandpic [label=<
brandpic
idINT PK
brand_idINT FK
pic_idINT FK
sortINT
>]; } subgraph cluster_commerce { label="Pricing & Documents"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; + price [label=<
price
idINT PK
tax_idINT FK
customergroup_idINT FK
article_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; + manifest [label=<
manifest
idINT PK
entity_id?INT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
dateDATETIME2
>]; + manifestarticle [label=<
manifestarticle
idINT PK
manifest_idINT FK
tax_idINT FK
article_id?INT FK
amountFLOAT
netFLOAT
transactedamountFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
>]; tax [label=<
tax
idINT PK
rateDECIMAL(18,4)
>]; - price [label=<
price
idINT PK
article_idINT FK
customergroup_idINT FK
tax_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; - manifest [label=<
manifest
idINT PK
numberNVARCHAR(100)
typeNVARCHAR(100)
dateDATETIME2
>]; - manifestarticle [label=<
manifestarticle
idINT PK
article_id?INT FK
tax_idINT FK
manifest_idINT FK
amountFLOAT
netFLOAT
transactedamountFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
>]; } subgraph cluster_units { label="Units"; labeljust="l"; fontsize=15; @@ -72,56 +73,59 @@ digraph schema { __legend [label=<
Legend
PKprimary key
FKforeign key
?nullable column
—FOREIGN KEY constraint
- -naming convention only, no constraint
>]; - google_auth:selected_company:w -> company:in8:n [color="#c53030", fontcolor="#c53030", tooltip="FK_google_auth_selected_company"]; + google_auth:selected_company:sw -> company:in5:n [color="#c53030", fontcolor="#c53030", tooltip="FK_google_auth_selected_company"]; category:parent_id:e -> category:id:e [color="#2f855a", fontcolor="#2f855a", tooltip="FK_category_parent"]; - company:root_category_id:w -> category:in5:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_company_root_category"]; - company:default_language_id:w -> language:in3:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_company_default_language"]; - company:taxrate:e -> tax:in2:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_company_taxrate"]; - company:reducedtaxrate:e -> tax:in0:n [color="#c05621", fontcolor="#c05621", tooltip="FK_company_reducedtaxrate"]; - company:taxfreerate:e -> tax:in1:n [color="#319795", fontcolor="#319795", tooltip="FK_company_taxfreerate"]; - customergroup:company_id:nw -> company:in6:n [color="#97266d", fontcolor="#97266d", tooltip="FK_customergroup_company"]; - company:poscustomergroup_id:se -> customergroup:in1:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_company_poscustomergroup"]; - entity:customergroup_id:e -> customergroup:in0:n [color="#276749", fontcolor="#276749", tooltip="FK_entity_customergroup"]; - unit:company_id:e -> company:in2:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_unit_company"]; - unittext:unit_id:e -> unit:in0:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_unittext_unit"]; - unittext:language_id:sw -> language:in1:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_unittext_language"]; - brand:company_id:e -> company:in1:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_brand_company"]; - article:unit_id:e -> unit:in1:n [color="#22543d", fontcolor="#22543d", tooltip="FK_article_unit"]; - article:brand_id:sw -> brand:in1:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_article_brand"]; - article:articlelist_id:se -> articlelist:in1:n [color="#702459", fontcolor="#702459", tooltip="FK_article_articlelist"]; - article:optionslist_id:se -> optionslist:in0:n [color="#744210", fontcolor="#744210", tooltip="FK_article_optionslist"]; - article:fatherarticle_id:e -> article:id:e [color="#285e61", fontcolor="#285e61", tooltip="FK_article_fatherarticle"]; - articlehistory:article_id:sw -> article:in6:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_articlehistory_article"]; - articlehistory:companyuser_id:e -> companyuser:in0:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_articlehistory_companyuser"]; - articleean:article_id:se -> article:in4:n [color="#38a169", fontcolor="#38a169", tooltip="FK_articleean_article"]; - articlepic:article_id:e -> article:in1:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_articlepic_article"]; - articlepic:pic_id:sw -> pic:in1:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_articlepic_pic"]; - brandpic:brand_id:e -> brand:in0:n [color="#c53030", fontcolor="#c53030", tooltip="FK_brandpic_brand"]; - brandpic:pic_id:sw -> pic:in2:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_brandpic_pic"]; - articlelistarticle:articlelist_id:se -> articlelist:in0:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_articlelistarticle_articlelist"]; - articlelistarticle:article_id:se -> article:in3:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_articlelistarticle_article"]; - optionslistarticle:optionslist_id:sw -> optionslist:in1:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_optionslistarticle_optionslist"]; - optionslistarticle:article_id:sw -> article:in7:n [color="#c05621", fontcolor="#c05621", tooltip="FK_optionslistarticle_article"]; - categoryarticle:category_id:sw -> category:in4:n [color="#319795", fontcolor="#319795", tooltip="FK_categoryarticle_category"]; - categoryarticle:article_id:e -> article:in2:n [color="#97266d", fontcolor="#97266d", tooltip="FK_categoryarticle_article"]; - categorypic:category_id:se -> category:in2:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_categorypic_category"]; - categorypic:pic_id:se -> pic:in0:n [color="#276749", fontcolor="#276749", tooltip="FK_categorypic_pic"]; - categorytext:category_id:e -> category:in0:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_categorytext_category"]; - categorytext:language_id:sw -> language:in2:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_categorytext_language"]; - articletext:article_id:e -> article:in0:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_articletext_article"]; - articletext:language_id:sw -> language:in0:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_articletext_language"]; - price:tax_id:sw -> tax:in3:n [color="#22543d", fontcolor="#22543d", tooltip="FK_price_tax"]; - price:article_id:w -> article:in8:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_price_article"]; - price:customergroup_id:sw -> customergroup:in2:n [color="#702459", fontcolor="#702459", tooltip="FK_price_customergroup"]; - manifestarticle:manifest_id:sw -> manifest:in0:n [color="#744210", fontcolor="#744210", tooltip="FK_manifestarticle_manifest"]; - manifestarticle:article_id:w -> article:in9:n [color="#285e61", fontcolor="#285e61", tooltip="FK_manifestarticle_article"]; - manifestarticle:tax_id:sw -> tax:in4:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_manifestarticle_tax"]; - numbers:company_id:se -> company:in3:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_numbers_company"]; - companyuser:google_auth_id:e -> google_auth:in0:n [color="#38a169", fontcolor="#38a169", tooltip="FK_companyuser_google_auth"]; - companyuser:company_id:se -> company:in4:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_companyuser_company"]; - syncpos:company_id:se -> company:in5:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_syncpos_company"]; - companysetting:company_id:sw -> company:in7:n [color="#c53030", fontcolor="#c53030", tooltip="FK_companysetting_company"]; - posterminal:company_id:e -> company:in0:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_posterminal_company"]; - posterminalcategory:posterminal_id:se -> posterminal:in0:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_posterminalcategory_posterminal"]; - posterminalcategory:category_id:se -> category:in1:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_posterminalcategory_category"]; + company:root_category_id:e -> category:in0:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_company_root_category"]; + company:default_language_id:e -> language:in0:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_company_default_language"]; + company:taxrate:w -> tax:in4:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_company_taxrate"]; + company:reducedtaxrate:w -> tax:in2:n [color="#c05621", fontcolor="#c05621", tooltip="FK_company_reducedtaxrate"]; + company:taxfreerate:w -> tax:in3:n [color="#319795", fontcolor="#319795", tooltip="FK_company_taxfreerate"]; + customergroup:company_id:ne -> company:in1:n [color="#97266d", fontcolor="#97266d", tooltip="FK_customergroup_company"]; + company:poscustomergroup_id:sw -> customergroup:in2:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_company_poscustomergroup"]; + entity:customergroup_id:se -> customergroup:in1:n [color="#276749", fontcolor="#276749", tooltip="FK_entity_customergroup"]; + entityhistory:entity_id:sw -> entity:in1:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_entityhistory_entity"]; + entityhistory:companyuser_id:se -> companyuser:in0:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_entityhistory_companyuser"]; + unit:company_id:w -> company:in7:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_unit_company"]; + unittext:unit_id:w -> unit:in1:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_unittext_unit"]; + unittext:language_id:se -> language:in1:n [color="#22543d", fontcolor="#22543d", tooltip="FK_unittext_language"]; + brand:company_id:w -> company:in8:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_brand_company"]; + article:unit_id:w -> unit:in0:n [color="#702459", fontcolor="#702459", tooltip="FK_article_unit"]; + article:brand_id:se -> brand:in0:n [color="#744210", fontcolor="#744210", tooltip="FK_article_brand"]; + article:articlelist_id:sw -> articlelist:in1:n [color="#285e61", fontcolor="#285e61", tooltip="FK_article_articlelist"]; + article:optionslist_id:se -> optionslist:in0:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_article_optionslist"]; + article:fatherarticle_id:e -> article:id:e [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_article_fatherarticle"]; + articlehistory:article_id:sw -> article:in5:n [color="#38a169", fontcolor="#38a169", tooltip="FK_articlehistory_article"]; + articlehistory:companyuser_id:w -> companyuser:in1:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_articlehistory_companyuser"]; + articleean:article_id:sw -> article:in6:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_articleean_article"]; + articlepic:article_id:w -> article:in8:n [color="#c53030", fontcolor="#c53030", tooltip="FK_articlepic_article"]; + articlepic:pic_id:se -> pic:in1:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_articlepic_pic"]; + brandpic:brand_id:w -> brand:in1:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_brandpic_brand"]; + brandpic:pic_id:sw -> pic:in2:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_brandpic_pic"]; + articlelistarticle:articlelist_id:se -> articlelist:in0:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_articlelistarticle_articlelist"]; + articlelistarticle:article_id:se -> article:in3:n [color="#c05621", fontcolor="#c05621", tooltip="FK_articlelistarticle_article"]; + optionslistarticle:optionslist_id:sw -> optionslist:in1:n [color="#319795", fontcolor="#319795", tooltip="FK_optionslistarticle_optionslist"]; + optionslistarticle:article_id:w -> article:in7:n [color="#97266d", fontcolor="#97266d", tooltip="FK_optionslistarticle_article"]; + categoryarticle:category_id:sw -> category:in3:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_categoryarticle_category"]; + categoryarticle:article_id:se -> article:in2:n [color="#276749", fontcolor="#276749", tooltip="FK_categoryarticle_article"]; + categorypic:category_id:sw -> category:in4:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_categorypic_category"]; + categorypic:pic_id:se -> pic:in0:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_categorypic_pic"]; + categorytext:category_id:w -> category:in5:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_categorytext_category"]; + categorytext:language_id:sw -> language:in3:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_categorytext_language"]; + articletext:article_id:w -> article:in9:n [color="#22543d", fontcolor="#22543d", tooltip="FK_articletext_article"]; + articletext:language_id:sw -> language:in2:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_articletext_language"]; + price:tax_id:se -> tax:in0:n [color="#702459", fontcolor="#702459", tooltip="FK_price_tax"]; + price:article_id:e -> article:in0:n [color="#744210", fontcolor="#744210", tooltip="FK_price_article"]; + price:customergroup_id:se -> customergroup:in0:n [color="#285e61", fontcolor="#285e61", tooltip="FK_price_customergroup"]; + manifest:entity_id:se -> entity:in0:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_manifest_entity"]; + manifestarticle:manifest_id:sw -> manifest:in0:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_manifestarticle_manifest"]; + manifestarticle:article_id:e -> article:in1:n [color="#38a169", fontcolor="#38a169", tooltip="FK_manifestarticle_article"]; + manifestarticle:tax_id:se -> tax:in1:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_manifestarticle_tax"]; + numbers:company_id:se -> company:in0:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_numbers_company"]; + companyuser:google_auth_id:se -> google_auth:in0:n [color="#c53030", fontcolor="#c53030", tooltip="FK_companyuser_google_auth"]; + companyuser:company_id:sw -> company:in4:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_companyuser_company"]; + syncpos:company_id:se -> company:in2:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_syncpos_company"]; + companysetting:company_id:sw -> company:in3:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_companysetting_company"]; + posterminal:company_id:w -> company:in6:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_posterminal_company"]; + posterminalcategory:posterminal_id:se -> posterminal:in0:n [color="#c05621", fontcolor="#c05621", tooltip="FK_posterminalcategory_posterminal"]; + posterminalcategory:category_id:se -> category:in1:n [color="#319795", fontcolor="#319795", tooltip="FK_posterminalcategory_category"]; } \ No newline at end of file diff --git a/schema-graph.svg b/schema-graph.svg index 947dd0e..d1fa67f 100644 --- a/schema-graph.svg +++ b/schema-graph.svg @@ -4,777 +4,837 @@ - - + + schema - + cluster_identity - -Identity & Company + +Identity & Company cluster_i18n - -Localization + +Localization cluster_catalog - -Catalog + +Catalog cluster_media - -Media + +Media cluster_commerce - -Pricing & Documents + +Pricing & Documents cluster_units - -Units - - - -numbers - - -numbers - - -id -INT -PK - - -company_id -INT -FK - - -type -NVARCHAR(100) - - -prefix -NVARCHAR(50) - - -padding -INT - - -next -INT - - - -company - - - - - - - - - - - -company - - -id -INT -PK - - -default_language_id -? -INT -FK - - -root_category_id -? -INT -FK - - -poscustomergroup_id -INT -FK - - -taxrate -? -INT -FK - - -reducedtaxrate -? -INT -FK - - -taxfreerate -? -INT -FK - - -name -NVARCHAR(255) - - - -numbers:se->company:n - - - - - + +Units - + entity - - -entity - - -id -INT -PK - - -customergroup_id -INT -FK - - -firstname -NVARCHAR(255) - - -lastname -NVARCHAR(255) + + + + +entity + + +id +INT +PK + + +customergroup_id +INT +FK + + +first_name? +NVARCHAR(255) + + +name +NVARCHAR(255) + + +created_at +DATETIME2 + + +updated_at +DATETIME2 - + customergroup - - - - - -customergroup - - -id -INT -PK - - -company_id -? -INT -FK - - -name -NVARCHAR(255) + + + + + +customergroup + + +id +INT +PK + + +company_id +? +INT +FK + + +name +NVARCHAR(255) -entity:e->customergroup:n +entity:se->customergroup:n - - + + - + + +meta + + +meta + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + + -companyuser - - - -companyuser - - -id -INT -PK - - -company_id -INT -FK - - -google_auth_id -INT -FK +numbers + + +numbers + + +id +INT +PK + + +company_id +INT +FK + + +type +NVARCHAR(100) + + +prefix +NVARCHAR(50) + + +padding +INT + + +next +INT - - -companyuser:se->company:n - - - + + +company + + + + + + + + + + + +company + + +id +INT +PK + + +taxrate +? +INT +FK + + +reducedtaxrate +? +INT +FK + + +taxfreerate +? +INT +FK + + +poscustomergroup_id +INT +FK + + +root_category_id +? +INT +FK + + +default_language_id +? +INT +FK + + +name +NVARCHAR(255) + + + +numbers:se->company:n + + + - - -google_auth - - - -google_auth - - -id -INT -PK - - -selected_company -? -INT -FK - - -email -NVARCHAR(320) - - -picture? -NVARCHAR(2048) - - -picturebinary? -VARBINARY(MAX) - - -sub? -NVARCHAR(255) - - -created_at -DATETIME2 - - - -companyuser:e->google_auth:n - - - + + +customergroup:ne->company:n + + + - + syncpos - - -syncpos - - -id -INT -PK - - -company_id -INT -FK - - -paircode -NVARCHAR(50) - - -token? -NVARCHAR(255) + + +syncpos + + +id +INT +PK + + +company_id +INT +FK + + +paircode +NVARCHAR(50) + + +token? +NVARCHAR(255) - + syncpos:se->company:n - - - + + + -company:se->customergroup:n +company:sw->customergroup:n - - + + - + language - - - - - - -language - - -id -INT -PK - - -iso639 -NVARCHAR(10) + + + + + + +language + + +id +INT +PK + + +iso639 +NVARCHAR(10) -company:w->language:n +company:e->language:n - - + + - + category - - - - - - - - -category - - -id -INT -PK - - -parent_id -? -INT -FK - - -sort -INT + + + + + + + + +category + + +id +INT +PK + + +parent_id +? +INT +FK + + +sort +INT -company:w->category:n +company:e->category:n - - + + - + tax - - - - - - - -tax - - -id -INT -PK - - -rate -DECIMAL(18,4) + + + + + + + +tax + + +id +INT +PK + + +rate +DECIMAL(18,4) -company:e->tax:n +company:w->tax:n - - + + -company:e->tax:n +company:w->tax:n - - + + -company:e->tax:n +company:w->tax:n - - + + - - -meta - - -meta - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) + + +entityhistory + + +entityhistory + + +id +INT +PK + + +entity_id +INT +FK + + +companyuser_id +? +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) - - -customergroup:nw->company:n - - - + + +entityhistory:sw->entity:n + + + + + + + + +companyuser + + + + +companyuser + + +id +INT +PK + + +company_id +INT +FK + + +google_auth_id +INT +FK + + + +entityhistory:se->companyuser:n + + + companysetting - - -companysetting - - -id -INT -PK - - -company_id -INT -FK - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) - - -frontend -BIT + + +companysetting + + +id +INT +PK + + +company_id +INT +FK + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + + +frontend +BIT - + companysetting:sw->company:n - - - + + + + + + + + +companyuser:sw->company:n + + + + + + + + +google_auth + + + +google_auth + + +id +INT +PK + + +selected_company +? +INT +FK + + +email +NVARCHAR(320) + + +picture? +NVARCHAR(2048) + + +picturebinary? +VARBINARY(MAX) + + +sub? +NVARCHAR(255) + + +created_at +DATETIME2 + + + +companyuser:se->google_auth:n + + + -google_auth:w->company:n +google_auth:sw->company:n - - + + + + + + + +unittext + + +unittext + + +id +INT +PK + + +unit_id +INT +FK + + +language_id +INT +FK + + +symbol +NVARCHAR(50) + + +name +NVARCHAR(255) + + + +unittext:se->language:n + + + + + + + + +unit + + + + +unit + + +id +INT +PK + + +company_id +INT +FK + + +si +NVARCHAR(50) + + +basesi +NVARCHAR(50) + + +basesifactor +FLOAT + + + +unittext:w->unit:n + + + - + articletext - - -articletext - - -id -INT -PK - - -language_id -INT -FK - - -article_id -INT -FK - - -name -NVARCHAR(255) - - -slug -NVARCHAR(255) - - -seo? -NVARCHAR(MAX) - - -desc? -NVARCHAR(MAX) + + +articletext + + +id +INT +PK + + +article_id +INT +FK + + +language_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) - + articletext:sw->language:n - - - + + + article - - - - - - - - - - - - -article - - -id -INT -PK - - -brand_id -? -INT -FK - - -fatherarticle_id -? -INT -FK - - -articlelist_id -? -INT -FK - - -optionslist_id -? -INT -FK - - -unit_id -INT -FK - - -sku? -NVARCHAR(100) - - -unitamount -FLOAT - - -weight -FLOAT - - -is_father -BIT - - -fathersort -INT - - -created_at -DATETIME2 - - -updated_at -DATETIME2 + + + + + + + + + + + + +article + + +id +INT +PK + + +unit_id +INT +FK + + +articlelist_id +? +INT +FK + + +fatherarticle_id +? +INT +FK + + +optionslist_id +? +INT +FK + + +brand_id +? +INT +FK + + +sku? +NVARCHAR(100) + + +unitamount +FLOAT + + +weight +FLOAT + + +is_father +BIT + + +fathersort +INT + + +created_at +DATETIME2 + + +updated_at +DATETIME2 - -articletext:e->article:n - - - - - - - - -unittext - - -unittext - - -id -INT -PK - - -language_id -INT -FK - - -unit_id -INT -FK - - -symbol -NVARCHAR(50) - - -name -NVARCHAR(255) - - - -unittext:sw->language:n - - - - - - - - -unit - - - - -unit - - -id -INT -PK - - -company_id -INT -FK - - -si -NVARCHAR(50) - - -basesi -NVARCHAR(50) - - -basesifactor -FLOAT - - - -unittext:e->unit:n - - - + +articletext:w->article:n + + + - + categorytext - - -categorytext - - -id -INT -PK - - -language_id -INT -FK - - -category_id -INT -FK - - -name -NVARCHAR(255) - - -slug -NVARCHAR(255) - - -seo? -NVARCHAR(MAX) - - -desc? -NVARCHAR(MAX) + + +categorytext + + +id +INT +PK + + +category_id +INT +FK + + +language_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) - + categorytext:sw->language:n - - - + + + - -categorytext:e->category:n - - - + +categorytext:w->category:n + + + @@ -782,803 +842,818 @@ category:e->category:e - - + + - + categoryarticle - - -categoryarticle - - -id -INT -PK - - -category_id -INT -FK - - -article_id -INT -FK + + +categoryarticle + + +id +INT +PK + + +category_id +INT +FK + + +article_id +INT +FK - + categoryarticle:sw->category:n - - - + + + - -categoryarticle:e->article:n - - - + +categoryarticle:se->article:n + + + - + articlelistarticle - - -articlelistarticle - - -id -INT -PK - - -article_id -INT -FK - - -articlelist_id -INT -FK - - -count -INT - - -sort -INT - - - -articlelistarticle:se->article:n - - - - - + + +articlelistarticle + + +id +INT +PK + + +articlelist_id +INT +FK + + +article_id +INT +FK + + +count +INT + + +sort +INT - + articlelist - - - - -articlelist - - -id -INT -PK + + + + +articlelist + + +id +INT +PK - + articlelistarticle:se->articlelist:n - - - + + + - - -brand - - - - -brand - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -gsprpinfo? -NVARCHAR(MAX) - - - -brand:e->company:n - - - - - - - - -articleean - - -articleean - - -id -INT -PK - - -article_id -INT -FK - - -ean -NVARCHAR(50) - - - -articleean:se->article:n - - - - - - - - -article:sw->brand:n - - - - - - - - -article:e->article:e - - - + + +articlelistarticle:se->article:n + + + - -article:se->articlelist:n - - - + +article:sw->articlelist:n + + + + + + + + +article:e->article:e + + + - + optionslist - - - - -optionslist - - -id -INT -PK + + + + +optionslist + + +id +INT +PK - + article:se->optionslist:n - - - + + + + + + + + +brand + + + + +brand + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +gsprpinfo? +NVARCHAR(MAX) + + + +article:se->brand:n + + + - -article:e->unit:n - - - + +article:w->unit:n + + + - + articlehistory - - -articlehistory - - -id -INT -PK - - -article_id -INT -FK - - -companyuser_id -? -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) + + +articlehistory + + +id +INT +PK + + +companyuser_id +? +INT +FK + + +article_id +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) - -articlehistory:e->companyuser:n - - - + +articlehistory:w->companyuser:n + + + - + articlehistory:sw->article:n - - - + + + + + + + + +articleean + + +articleean + + +id +INT +PK + + +article_id +INT +FK + + +ean +NVARCHAR(50) + + + +articleean:sw->article:n + + + + + + + + +brand:w->company:n + + + - + optionslistarticle - - -optionslistarticle - - -id -INT -PK - - -article_id -INT -FK - - -optionslist_id -INT -FK - - -count -INT - - -sort -INT - - -mandated -BIT + + +optionslistarticle + + +id +INT +PK + + +article_id +INT +FK + + +optionslist_id +INT +FK + + +count +INT + + +sort +INT + + +mandated +BIT - -optionslistarticle:sw->article:n - - - + +optionslistarticle:w->article:n + + + - + optionslistarticle:sw->optionslist:n - - - + + + - + categorypic - - -categorypic - - -id -INT -PK - - -pic_id -INT -FK - - -category_id -INT -FK - - -sort -INT + + +categorypic + + +id +INT +PK + + +category_id +INT +FK + + +pic_id +INT +FK + + +sort +INT - -categorypic:se->category:n - - - + +categorypic:sw->category:n + + + - + pic - - - - - -pic - - -id -INT -PK - - -w -INT - - -h -INT - - -size -BIGINT - - -hash -NVARCHAR(64) - - -thumbw -INT - - -thumbh -INT - - -thumbhash -NVARCHAR(64) - - -source -NVARCHAR(MAX) - - -fullbinary -VARBINARY(MAX) - - -thumbbinary -VARBINARY(MAX) + + + + + +pic + + +id +INT +PK + + +w +INT + + +h +INT + + +size +BIGINT + + +hash +NVARCHAR(64) + + +thumbw +INT + + +thumbh +INT + + +thumbhash +NVARCHAR(64) + + +source +NVARCHAR(MAX) + + +fullbinary +VARBINARY(MAX) + + +thumbbinary +VARBINARY(MAX) - + categorypic:se->pic:n - - - + + + articlepic - - -articlepic - - -id -INT -PK - - -pic_id -INT -FK - - -article_id -INT -FK - - -sort -INT + + +articlepic + + +id +INT +PK + + +article_id +INT +FK + + +pic_id +INT +FK + + +sort +INT - -articlepic:e->article:n - - - + +articlepic:w->article:n + + + - -articlepic:sw->pic:n - - - + +articlepic:se->pic:n + + + - + brandpic - - -brandpic - - -id -INT -PK - - -pic_id -INT -FK - - -brand_id -INT -FK - - -sort -INT + + +brandpic + + +id +INT +PK + + +brand_id +INT +FK + + +pic_id +INT +FK + + +sort +INT - -brandpic:e->brand:n - - - + +brandpic:w->brand:n + + + - + brandpic:sw->pic:n - - - + + + price - - -price - - -id -INT -PK - - -article_id -INT -FK - - -customergroup_id -INT -FK - - -tax_id -INT -FK - - -minmultiplier -DECIMAL(18,4) - - -net -FLOAT + + +price + + +id +INT +PK + + +tax_id +INT +FK + + +customergroup_id +INT +FK + + +article_id +INT +FK + + +minmultiplier +DECIMAL(18,4) + + +net +FLOAT - -price:sw->customergroup:n - - - + +price:se->customergroup:n + + + - -price:w->article:n - - - + +price:e->article:n + + + - -price:sw->tax:n - - - + +price:se->tax:n + + + manifest - - - -manifest - - -id -INT -PK - - -number -NVARCHAR(100) - - -type -NVARCHAR(100) - - -date -DATETIME2 + + + +manifest + + +id +INT +PK + + +entity_id +? +INT +FK + + +number +NVARCHAR(100) + + +type +NVARCHAR(100) + + +date +DATETIME2 + + + +manifest:se->entity:n + + + + + manifestarticle - - -manifestarticle - - -id -INT -PK - - -article_id -? -INT -FK - - -tax_id -INT -FK - - -manifest_id -INT -FK - - -amount -FLOAT - - -net -FLOAT - - -transactedamount -FLOAT - - -comment? -NVARCHAR(MAX) - - -rebate -FLOAT + + +manifestarticle + + +id +INT +PK + + +manifest_id +INT +FK + + +tax_id +INT +FK + + +article_id +? +INT +FK + + +amount +FLOAT + + +net +FLOAT + + +transactedamount +FLOAT + + +comment? +NVARCHAR(MAX) + + +rebate +FLOAT - -manifestarticle:w->article:n - - - - - - - - -manifestarticle:sw->tax:n - - - + +manifestarticle:e->article:n + + + - + manifestarticle:sw->manifest:n - - - + + + + + + + + +manifestarticle:se->tax:n + + + - -unit:e->company:n - - - + +unit:w->company:n + + + - + posterminalcategory - - -posterminalcategory - - -id -INT -PK - - -posterminal_id -INT -FK - - -category_id -INT -FK + + +posterminalcategory + + +id +INT +PK + + +posterminal_id +INT +FK + + +category_id +INT +FK - + posterminalcategory:se->category:n - - - + + + - + posterminal - - - -posterminal - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -token? -NVARCHAR(255) - - -pairingpin -NVARCHAR(50) + + + +posterminal + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +token? +NVARCHAR(255) + + +pairingpin +NVARCHAR(50) - + posterminalcategory:se->posterminal:n - - - + + + - -posterminal:e->company:n - - - + +posterminal:w->company:n + + + - + __legend - - - -Legend - -PK - -primary key - -FK - -foreign key - -? - -nullable column - -— - -FOREIGN KEY constraint - -- - - -naming convention only, no constraint + + + +Legend + +PK + +primary key + +FK + +foreign key + +? + +nullable column + +— + +FOREIGN KEY constraint + +- - + +naming convention only, no constraint diff --git a/scripts/test-api.js b/scripts/test-api.js index 2fcc5c0..7993ed2 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -158,6 +158,31 @@ const MENU = { }, ], }, + supplier: { + label: 'supplier', + items: [ + { + value: 'supplier.list', + label: 'list', + hint: 'googleAuthId, companyId → rows', + }, + { + value: 'supplier.create', + label: 'create', + hint: 'googleAuthId, companyId, name → row', + }, + { + value: 'supplier.update', + label: 'update', + hint: 'googleAuthId, companyId, id, name → row', + }, + { + value: 'supplier.delete', + label: 'delete', + hint: 'googleAuthId, companyId, id', + }, + ], + }, category: { label: 'category', items: [ @@ -626,6 +651,67 @@ async function runMethod(client, action) { await client.brand.delete(Number(googleAuthId), Number(companyId), id); return ['ok', true]; } + case 'supplier.list': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + return [ + 'result[]', + await client.supplier.list(Number(googleAuthId), Number(companyId)), + ]; + } + case 'supplier.create': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const name = await askText('name', { + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (name == null) return; + return [ + 'result', + await client.supplier.create(Number(googleAuthId), Number(companyId), { + name, + }), + ]; + } + case 'supplier.update': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const id = await askInt('supplier id'); + if (id == null) return; + const name = await askText('name', { + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (name == null) return; + return [ + 'result', + await client.supplier.update( + Number(googleAuthId), + Number(companyId), + Number(id), + { name }, + ), + ]; + } + case 'supplier.delete': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const id = await askInt('supplier id'); + if (id == null) return; + await client.supplier.delete( + Number(googleAuthId), + Number(companyId), + Number(id), + ); + return ['ok', true]; + } case 'lang.create': { const iso639 = await askText('ISO 639', { placeholder: 'de', diff --git a/src/index.js b/src/index.js index 2e0694b..92d48ed 100644 --- a/src/index.js +++ b/src/index.js @@ -39,6 +39,10 @@ import companysettingDelete from './store/companysetting/delete.js'; import brandList from './store/brand/list.js'; import brandUpsert from './store/brand/upsert.js'; import brandDelete from './store/brand/delete.js'; +import supplierList from './store/supplier/list.js'; +import supplierCreate from './store/supplier/create.js'; +import supplierUpdate from './store/supplier/update.js'; +import supplierDelete from './store/supplier/delete.js'; import posterminalList from './store/posterminal/list.js'; import posterminalUpsert from './store/posterminal/upsert.js'; import posterminalDelete from './store/posterminal/delete.js'; @@ -149,6 +153,13 @@ export default class LibDashStore { delete: brandDelete(pool), }; + this.supplier = { + list: supplierList(pool), + create: supplierCreate(pool), + update: supplierUpdate(pool), + delete: supplierDelete(pool), + }; + this.posterminal = { list: posterminalList(pool), upsert: posterminalUpsert(pool), diff --git a/src/schema.js b/src/schema.js index 85a3ee7..c83ff48 100644 --- a/src/schema.js +++ b/src/schema.js @@ -155,13 +155,30 @@ BEGIN CREATE TABLE entity ( id INT IDENTITY(1,1) PRIMARY KEY, customergroup_id INT NOT NULL, - firstname NVARCHAR(255) NOT NULL, - lastname NVARCHAR(255) NOT NULL, + first_name NVARCHAR(255) NULL, + name NVARCHAR(255) NOT NULL, + created_at DATETIME2 NOT NULL CONSTRAINT DF_entity_created_at DEFAULT SYSUTCDATETIME(), + updated_at DATETIME2 NOT NULL CONSTRAINT DF_entity_updated_at DEFAULT SYSUTCDATETIME(), CONSTRAINT FK_entity_customergroup FOREIGN KEY (customergroup_id) REFERENCES customergroup(id) ON DELETE CASCADE ); END `; +export const INIT_ENTITYHISTORY = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'entityhistory') +BEGIN + CREATE TABLE entityhistory ( + id INT IDENTITY(1,1) PRIMARY KEY, + entity_id INT NOT NULL, + companyuser_id INT NULL, + [date] DATETIME2 NOT NULL CONSTRAINT DF_entityhistory_date DEFAULT SYSUTCDATETIME(), + [text] NVARCHAR(MAX) NOT NULL, + CONSTRAINT FK_entityhistory_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE, + CONSTRAINT FK_entityhistory_companyuser FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE NO ACTION + ); +END +`; + export const INIT_UNIT = ` IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'unit') BEGIN @@ -453,7 +470,9 @@ BEGIN id INT IDENTITY(1,1) PRIMARY KEY, [number] NVARCHAR(100) NOT NULL, [type] NVARCHAR(100) NOT NULL, - [date] DATETIME2 NOT NULL CONSTRAINT DF_manifest_date DEFAULT SYSUTCDATETIME() + entity_id INT NULL, + [date] DATETIME2 NOT NULL CONSTRAINT DF_manifest_date DEFAULT SYSUTCDATETIME(), + CONSTRAINT FK_manifest_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL ); END `; @@ -637,6 +656,7 @@ export const SCHEMA_SCRIPTS = [ INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING, INIT_COMPANYUSER, INIT_ARTICLEHISTORY, + INIT_ENTITYHISTORY, INIT_SYNCPOS, INIT_COMPANYSETTING, INIT_POSTERMINAL, diff --git a/src/sql/customergroup.js b/src/sql/customergroup.js index a0ffd5a..e5cf5e0 100644 --- a/src/sql/customergroup.js +++ b/src/sql/customergroup.js @@ -15,3 +15,9 @@ UPDATE customergroup SET company_id = @companyId WHERE id = @id `; + +export const GET_BY_COMPANY_AND_NAME = ` +SELECT id +FROM customergroup +WHERE company_id = @companyId AND name = @name +`; diff --git a/src/sql/entity.js b/src/sql/entity.js new file mode 100644 index 0000000..9fc2b2a --- /dev/null +++ b/src/sql/entity.js @@ -0,0 +1,59 @@ +export const SUPPLIER_GROUP_NAME = 'Lieferanten'; + +export const LIST_SUPPLIERS_BY_COMPANY = ` +SELECT + e.id, + e.name, + e.created_at AS createdAt, + e.updated_at AS updatedAt +FROM entity e +INNER JOIN customergroup cg ON cg.id = e.customergroup_id +WHERE cg.company_id = @companyId + AND cg.name = @groupName +ORDER BY e.name, e.id +`; + +export const GET_SUPPLIER_BY_ID = ` +SELECT + e.id, + e.name, + e.created_at AS createdAt, + e.updated_at AS updatedAt +FROM entity e +INNER JOIN customergroup cg ON cg.id = e.customergroup_id +WHERE e.id = @id + AND cg.company_id = @companyId + AND cg.name = @groupName +`; + +export const CREATE = ` +INSERT INTO entity (customergroup_id, name) +OUTPUT INSERTED.id, INSERTED.name, INSERTED.created_at AS createdAt, INSERTED.updated_at AS updatedAt +VALUES (@customergroupId, @name) +`; + +export const UPDATE_NAME = ` +UPDATE entity +SET name = @name, updated_at = SYSUTCDATETIME() +OUTPUT INSERTED.id, INSERTED.name, INSERTED.created_at AS createdAt, INSERTED.updated_at AS updatedAt +WHERE id = @id + AND customergroup_id = @customergroupId +`; + +export const DELETE_BY_ID = ` +DELETE e +FROM entity e +INNER JOIN customergroup cg ON cg.id = e.customergroup_id +WHERE e.id = @id + AND cg.company_id = @companyId + AND cg.name = @groupName +`; + +export const GET_GROUP_ID_FOR_SUPPLIER = ` +SELECT e.customergroup_id AS customergroupId, e.name +FROM entity e +INNER JOIN customergroup cg ON cg.id = e.customergroup_id +WHERE e.id = @id + AND cg.company_id = @companyId + AND cg.name = @groupName +`; diff --git a/src/sql/entityhistory.js b/src/sql/entityhistory.js new file mode 100644 index 0000000..dd823ad --- /dev/null +++ b/src/sql/entityhistory.js @@ -0,0 +1,11 @@ +export const INSERT = ` +INSERT INTO entityhistory (entity_id, companyuser_id, [text]) +VALUES (@entityId, @companyuserId, @text) +`; + +export const LIST_BY_ENTITY_ID = ` +SELECT id, companyuser_id AS companyuserId, [date], [text] +FROM entityhistory +WHERE entity_id = @entityId +ORDER BY [date] DESC, id DESC +`; diff --git a/src/sql/purge.js b/src/sql/purge.js index 54b2571..38f2159 100644 --- a/src/sql/purge.js +++ b/src/sql/purge.js @@ -19,6 +19,7 @@ export const TABLES = [ 'articlepic', 'articleean', 'articlehistory', + 'entityhistory', 'article', 'brand', 'articlelist', diff --git a/src/store/companyuser/resolveCompanyAccess.js b/src/store/companyuser/resolveCompanyAccess.js index f21b04d..20a5ad4 100644 --- a/src/store/companyuser/resolveCompanyAccess.js +++ b/src/store/companyuser/resolveCompanyAccess.js @@ -23,6 +23,7 @@ export default function resolveCompanyAccess(pool) { rootCategoryId: row.rootCategoryId, languageId: row.languageId, posCustomerGroupId: row.posCustomerGroupId ?? null, + companyuserId: row.companyuserId, }; }; } diff --git a/src/store/supplier/create.js b/src/store/supplier/create.js new file mode 100644 index 0000000..4787bef --- /dev/null +++ b/src/store/supplier/create.js @@ -0,0 +1,54 @@ +import { CREATE } from '../../sql/entity.js'; +import { INSERT as INSERT_HISTORY } from '../../sql/entityhistory.js'; +import request from '../request.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import ensureLieferantenGroup from './ensureLieferantenGroup.js'; + +export default function create(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, { name } = {}) => { + const cleaned = name?.trim(); + if (!cleaned) { + throw new Error('Name required'); + } + + const { companyuserId } = await access(googleAuthId, companyId); + + const tx = pool.transaction(); + await tx.begin(); + + try { + const customergroupId = await ensureLieferantenGroup(pool, companyId, tx); + + const created = await request(pool, tx) + .input('customergroupId', customergroupId) + .input('name', cleaned) + .query(CREATE); + + const row = created.recordset[0]; + + await request(pool, tx) + .input('entityId', row.id) + .input('companyuserId', companyuserId) + .input('text', 'Erstellt') + .query(INSERT_HISTORY); + + await tx.commit(); + + return { + id: row.id, + name: row.name, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + }; +} diff --git a/src/store/supplier/delete.js b/src/store/supplier/delete.js new file mode 100644 index 0000000..d09a58c --- /dev/null +++ b/src/store/supplier/delete.js @@ -0,0 +1,25 @@ +import { DELETE_BY_ID, SUPPLIER_GROUP_NAME } from '../../sql/entity.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function del(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, id) => { + if (!Number.isFinite(id)) { + throw new Error('Supplier not found'); + } + + await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('id', id) + .input('companyId', companyId) + .input('groupName', SUPPLIER_GROUP_NAME) + .query(DELETE_BY_ID); + + if (!result.rowsAffected[0]) { + throw new Error('Supplier not found'); + } + }; +} diff --git a/src/store/supplier/ensureLieferantenGroup.js b/src/store/supplier/ensureLieferantenGroup.js new file mode 100644 index 0000000..bc7d97d --- /dev/null +++ b/src/store/supplier/ensureLieferantenGroup.js @@ -0,0 +1,26 @@ +import { + CREATE, + GET_BY_COMPANY_AND_NAME, +} from '../../sql/customergroup.js'; +import { SUPPLIER_GROUP_NAME } from '../../sql/entity.js'; + +/** Ensure company has customergroup "Lieferanten"; return its id. */ +export default async function ensureLieferantenGroup(pool, companyId, tx) { + const req = () => (tx ?? pool).request(); + + const existing = await req() + .input('companyId', companyId) + .input('name', SUPPLIER_GROUP_NAME) + .query(GET_BY_COMPANY_AND_NAME); + + if (existing.recordset[0]) { + return existing.recordset[0].id; + } + + const created = await req() + .input('companyId', companyId) + .input('name', SUPPLIER_GROUP_NAME) + .query(CREATE); + + return created.recordset[0].id; +} diff --git a/src/store/supplier/list.js b/src/store/supplier/list.js new file mode 100644 index 0000000..0481bbf --- /dev/null +++ b/src/store/supplier/list.js @@ -0,0 +1,23 @@ +import { LIST_SUPPLIERS_BY_COMPANY, SUPPLIER_GROUP_NAME } from '../../sql/entity.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function list(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId) => { + await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('companyId', companyId) + .input('groupName', SUPPLIER_GROUP_NAME) + .query(LIST_SUPPLIERS_BY_COMPANY); + + return result.recordset.map((row) => ({ + id: row.id, + name: row.name, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + })); + }; +} diff --git a/src/store/supplier/update.js b/src/store/supplier/update.js new file mode 100644 index 0000000..eaddc83 --- /dev/null +++ b/src/store/supplier/update.js @@ -0,0 +1,93 @@ +import { + GET_GROUP_ID_FOR_SUPPLIER, + GET_SUPPLIER_BY_ID, + SUPPLIER_GROUP_NAME, + UPDATE_NAME, +} from '../../sql/entity.js'; +import { INSERT as INSERT_HISTORY } from '../../sql/entityhistory.js'; +import request from '../request.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +function shape(row) { + return { + id: row.id, + name: row.name, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +export default function update(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, id, { name } = {}) => { + if (!Number.isFinite(id)) { + throw new Error('Supplier not found'); + } + + const cleaned = name?.trim(); + if (!cleaned) { + throw new Error('Name required'); + } + + const { companyuserId } = await access(googleAuthId, companyId); + + const existing = await pool + .request() + .input('id', id) + .input('companyId', companyId) + .input('groupName', SUPPLIER_GROUP_NAME) + .query(GET_GROUP_ID_FOR_SUPPLIER); + + const current = existing.recordset[0]; + if (!current) { + throw new Error('Supplier not found'); + } + + if (current.name === cleaned) { + const same = await pool + .request() + .input('id', id) + .input('companyId', companyId) + .input('groupName', SUPPLIER_GROUP_NAME) + .query(GET_SUPPLIER_BY_ID); + return shape(same.recordset[0]); + } + + const tx = pool.transaction(); + await tx.begin(); + + try { + const updated = await request(pool, tx) + .input('id', id) + .input('customergroupId', current.customergroupId) + .input('name', cleaned) + .query(UPDATE_NAME); + + const row = updated.recordset[0]; + if (!row) { + throw new Error('Supplier not found'); + } + + await request(pool, tx) + .input('entityId', id) + .input('companyuserId', companyuserId) + .input( + 'text', + `Name geändert von "${current.name}" auf "${cleaned}"`, + ) + .query(INSERT_HISTORY); + + await tx.commit(); + + return shape(row); + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + }; +}