feat: Add password reset functionality with request and reset forms

feat: Implement price list import feature with preview and apply options

feat: Create price rules management page with CRUD operations

feat: Develop quotes management page with itemized quotes and status tracking

feat: Introduce organization registration page for new users

feat: Build suppliers management page with detailed supplier information

feat: Create users management page for inviting and managing roles

chore: Add TypeScript configuration for improved type checking

chore: Set up Vite configuration for development server and API proxy

chore: Add Vite environment type definitions for better TypeScript support
This commit is contained in:
Torsten Schulz (local)
2026-06-02 15:28:38 +02:00
commit 0e539710c0
95 changed files with 31882 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
-- Template migration for each organization schema.
-- Replace {schema} with the real schema name, e.g. company_<organization_id>.
create table if not exists {schema}.settings (
key text primary key,
value_ciphertext bytea not null,
value_nonce bytea not null,
value_key_id text not null,
updated_by_user_id uuid,
updated_at timestamptz not null default now()
);
create table if not exists {schema}.roles (
id uuid primary key,
code text not null unique,
name text not null,
description text,
is_system_role boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists {schema}.permissions (
id uuid primary key,
code text not null unique,
description text
);
create table if not exists {schema}.role_permissions (
role_id uuid not null references {schema}.roles(id) on delete cascade,
permission_id uuid not null references {schema}.permissions(id) on delete cascade,
primary key (role_id, permission_id)
);
create table if not exists {schema}.user_roles (
user_id uuid not null,
role_id uuid not null references {schema}.roles(id) on delete cascade,
created_at timestamptz not null default now(),
primary key (user_id, role_id)
);
create table if not exists {schema}.number_ranges (
id uuid primary key,
code text not null unique,
pattern text not null,
counter_value bigint not null default 0,
counter_padding integer not null default 0,
reset_rule text,
is_active boolean not null default true,
updated_at timestamptz not null default now(),
constraint number_ranges_pattern_has_counter check (position('{counter}' in pattern) > 0)
);
create table if not exists {schema}.audit_log (
id uuid primary key,
actor_user_id uuid,
action text not null,
entity_type text not null,
entity_id uuid,
before_ciphertext bytea,
before_nonce bytea,
before_key_id text,
after_ciphertext bytea,
after_nonce bytea,
after_key_id text,
created_at timestamptz not null default now()
);
create table if not exists {schema}.change_log (
id uuid primary key,
sequence bigserial not null unique,
entity_type text not null,
entity_id uuid not null,
operation text not null,
payload_ciphertext bytea not null,
payload_nonce bytea not null,
payload_key_id text not null,
created_at timestamptz not null default now(),
created_by_user_id uuid
);

View File

@@ -0,0 +1,360 @@
-- Template migration for each organization schema.
-- Replace {schema} with the real schema name, e.g. company_<organization_id>.
create table if not exists {schema}.customers (
id uuid primary key,
customer_number text unique,
name_ciphertext bytea not null,
name_nonce bytea not null,
name_key_id text not null,
status text not null default 'active',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint customers_status_valid check (status in ('active', 'inactive', 'blocked'))
);
create table if not exists {schema}.suppliers (
id uuid primary key,
supplier_number text unique,
name_ciphertext bytea not null,
name_nonce bytea not null,
name_key_id text not null,
status text not null default 'active',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint suppliers_status_valid check (status in ('active', 'inactive', 'blocked'))
);
create table if not exists {schema}.items (
id uuid primary key,
item_number text not null unique,
name_ciphertext bytea not null,
name_nonce bytea not null,
name_key_id text not null,
unit text not null default 'Stk',
tax_rate numeric(7, 4) not null default 19.0,
default_purchase_price numeric(14, 4),
default_sales_price numeric(14, 4),
status text not null default 'active',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint items_status_valid check (status in ('active', 'inactive', 'blocked')),
constraint items_tax_rate_non_negative check (tax_rate >= 0),
constraint items_default_purchase_price_non_negative check (
default_purchase_price is null or default_purchase_price >= 0
),
constraint items_default_sales_price_non_negative check (
default_sales_price is null or default_sales_price >= 0
)
);
create table if not exists {schema}.cash_discount_terms (
id uuid primary key,
code text not null unique,
name text not null,
discount_percent numeric(7, 4) not null,
discount_days integer not null,
net_days integer,
valid_from date,
valid_until date,
is_default_customer_term boolean not null default false,
is_default_supplier_term boolean not null default false,
is_active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint cash_discount_terms_percent_valid check (
discount_percent >= 0 and discount_percent <= 100
),
constraint cash_discount_terms_days_valid check (
discount_days >= 0 and (net_days is null or net_days >= discount_days)
),
constraint cash_discount_terms_valid_range check (
valid_until is null or valid_from is null or valid_until >= valid_from
)
);
create unique index if not exists idx_cash_discount_terms_default_customer
on {schema}.cash_discount_terms (is_default_customer_term)
where is_default_customer_term;
create unique index if not exists idx_cash_discount_terms_default_supplier
on {schema}.cash_discount_terms (is_default_supplier_term)
where is_default_supplier_term;
create table if not exists {schema}.customer_price_terms (
id uuid primary key,
customer_id uuid not null references {schema}.customers(id) on delete cascade,
standard_discount_percent numeric(7, 4) not null default 0,
cash_discount_term_id uuid references {schema}.cash_discount_terms(id),
valid_from date,
valid_until date,
is_active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint customer_price_terms_discount_valid check (
standard_discount_percent >= 0 and standard_discount_percent <= 100
),
constraint customer_price_terms_valid_range check (
valid_until is null or valid_from is null or valid_until >= valid_from
)
);
create index if not exists idx_customer_price_terms_customer_active
on {schema}.customer_price_terms (customer_id, is_active, valid_from, valid_until);
create table if not exists {schema}.supplier_price_terms (
id uuid primary key,
supplier_id uuid not null references {schema}.suppliers(id) on delete cascade,
standard_discount_percent numeric(7, 4) not null default 0,
cash_discount_term_id uuid references {schema}.cash_discount_terms(id),
payment_days integer,
valid_from date,
valid_until date,
is_active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint supplier_price_terms_discount_valid check (
standard_discount_percent >= 0 and standard_discount_percent <= 100
),
constraint supplier_price_terms_payment_days_valid check (
payment_days is null or payment_days >= 0
),
constraint supplier_price_terms_valid_range check (
valid_until is null or valid_from is null or valid_until >= valid_from
)
);
create index if not exists idx_supplier_price_terms_supplier_active
on {schema}.supplier_price_terms (supplier_id, is_active, valid_from, valid_until);
create table if not exists {schema}.activities (
id uuid primary key,
activity_type text not null,
title_ciphertext bytea not null,
title_nonce bytea not null,
title_key_id text not null,
body_ciphertext bytea,
body_nonce bytea,
body_key_id text,
status text not null default 'open',
priority text not null default 'normal',
due_at timestamptz,
starts_at timestamptz,
ends_at timestamptz,
assigned_to_user_id uuid,
created_by_user_id uuid,
completed_by_user_id uuid,
completed_at timestamptz,
visibility text not null default 'internal',
system_source text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint activities_type_valid check (
activity_type in (
'email_note',
'phone_note',
'internal_note',
'task',
'follow_up',
'calendar_event',
'system_event',
'work_step'
)
),
constraint activities_status_valid check (
status in ('open', 'in_progress', 'done', 'cancelled')
),
constraint activities_priority_valid check (
priority in ('low', 'normal', 'high', 'critical')
),
constraint activities_visibility_valid check (
visibility in ('internal', 'organization')
),
constraint activities_body_encryption_complete check (
(
body_ciphertext is null
and body_nonce is null
and body_key_id is null
)
or (
body_ciphertext is not null
and body_nonce is not null
and body_key_id is not null
)
),
constraint activities_time_range_valid check (
ends_at is null or starts_at is null or ends_at >= starts_at
)
);
create index if not exists idx_activities_status_due
on {schema}.activities (status, due_at);
create index if not exists idx_activities_assigned_status
on {schema}.activities (assigned_to_user_id, status);
create table if not exists {schema}.activity_links (
activity_id uuid not null references {schema}.activities(id) on delete cascade,
entity_type text not null,
entity_id uuid not null,
created_at timestamptz not null default now(),
primary key (activity_id, entity_type, entity_id),
constraint activity_links_entity_type_valid check (
entity_type in (
'customer',
'supplier',
'contact',
'quote',
'outgoing_invoice',
'incoming_invoice',
'item',
'document',
'import'
)
)
);
create index if not exists idx_activity_links_entity
on {schema}.activity_links (entity_type, entity_id);
create table if not exists {schema}.outgoing_invoices (
id uuid primary key,
invoice_number text unique,
customer_id uuid not null references {schema}.customers(id),
status text not null default 'draft',
cash_discount_term_id uuid references {schema}.cash_discount_terms(id),
issued_at date,
due_at date,
created_by_user_id uuid,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
finalized_at timestamptz,
constraint outgoing_invoices_status_valid check (
status in ('draft', 'finalized', 'sent', 'paid', 'cancelled', 'overdue')
)
);
create index if not exists idx_outgoing_invoices_customer_status
on {schema}.outgoing_invoices (customer_id, status);
create table if not exists {schema}.outgoing_invoice_items (
id uuid primary key,
invoice_id uuid not null references {schema}.outgoing_invoices(id) on delete cascade,
line_number integer not null,
item_id uuid not null references {schema}.items(id),
description_ciphertext bytea,
description_nonce bytea,
description_key_id text,
quantity numeric(14, 4) not null,
unit_price numeric(14, 4) not null,
original_unit_price numeric(14, 4),
discount_percent numeric(7, 4) not null default 0,
price_overridden boolean not null default false,
price_override_reason_ciphertext bytea,
price_override_reason_nonce bytea,
price_override_reason_key_id text,
price_overridden_by_user_id uuid,
price_overridden_at timestamptz,
tax_rate numeric(7, 4) not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (invoice_id, line_number),
constraint outgoing_invoice_items_quantity_positive check (quantity > 0),
constraint outgoing_invoice_items_unit_price_non_negative check (unit_price >= 0),
constraint outgoing_invoice_items_original_price_non_negative check (
original_unit_price is null or original_unit_price >= 0
),
constraint outgoing_invoice_items_discount_valid check (
discount_percent >= 0 and discount_percent <= 100
),
constraint outgoing_invoice_items_tax_rate_non_negative check (tax_rate >= 0),
constraint outgoing_invoice_items_description_encryption_complete check (
(
description_ciphertext is null
and description_nonce is null
and description_key_id is null
)
or (
description_ciphertext is not null
and description_nonce is not null
and description_key_id is not null
)
),
constraint outgoing_invoice_items_override_reason_encryption_complete check (
(
price_override_reason_ciphertext is null
and price_override_reason_nonce is null
and price_override_reason_key_id is null
)
or (
price_override_reason_ciphertext is not null
and price_override_reason_nonce is not null
and price_override_reason_key_id is not null
)
),
constraint outgoing_invoice_items_override_complete check (
(
price_overridden = false
and price_overridden_by_user_id is null
and price_overridden_at is null
)
or (
price_overridden = true
and price_overridden_by_user_id is not null
and price_overridden_at is not null
)
)
);
create index if not exists idx_outgoing_invoice_items_item
on {schema}.outgoing_invoice_items (item_id);
create table if not exists {schema}.incoming_invoices (
id uuid primary key,
invoice_number text,
supplier_id uuid not null references {schema}.suppliers(id),
status text not null default 'draft',
cash_discount_term_id uuid references {schema}.cash_discount_terms(id),
invoice_date date,
due_at date,
created_by_user_id uuid,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint incoming_invoices_status_valid check (
status in ('draft', 'received', 'approved', 'paid', 'cancelled', 'overdue')
)
);
create index if not exists idx_incoming_invoices_supplier_status
on {schema}.incoming_invoices (supplier_id, status);
create table if not exists {schema}.incoming_invoice_items (
id uuid primary key,
invoice_id uuid not null references {schema}.incoming_invoices(id) on delete cascade,
line_number integer not null,
item_id uuid references {schema}.items(id),
description_ciphertext bytea,
description_nonce bytea,
description_key_id text,
quantity numeric(14, 4) not null,
unit_price numeric(14, 4) not null,
tax_rate numeric(7, 4) not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (invoice_id, line_number),
constraint incoming_invoice_items_quantity_positive check (quantity > 0),
constraint incoming_invoice_items_unit_price_non_negative check (unit_price >= 0),
constraint incoming_invoice_items_tax_rate_non_negative check (tax_rate >= 0),
constraint incoming_invoice_items_description_encryption_complete check (
(
description_ciphertext is null
and description_nonce is null
and description_key_id is null
)
or (
description_ciphertext is not null
and description_nonce is not null
and description_key_id is not null
)
)
);

View File

@@ -0,0 +1,45 @@
-- Additional encrypted customer fields for organization schemas.
alter table {schema}.customers
add column if not exists details_ciphertext bytea,
add column if not exists details_nonce bytea,
add column if not exists details_key_id text;
alter table {schema}.customers
drop constraint if exists customers_details_encryption_complete;
alter table {schema}.customers
add constraint customers_details_encryption_complete check (
(
details_ciphertext is null
and details_nonce is null
and details_key_id is null
)
or (
details_ciphertext is not null
and details_nonce is not null
and details_key_id is not null
)
);
alter table {schema}.suppliers
add column if not exists details_ciphertext bytea,
add column if not exists details_nonce bytea,
add column if not exists details_key_id text;
alter table {schema}.suppliers
drop constraint if exists suppliers_details_encryption_complete;
alter table {schema}.suppliers
add constraint suppliers_details_encryption_complete check (
(
details_ciphertext is null
and details_nonce is null
and details_key_id is null
)
or (
details_ciphertext is not null
and details_nonce is not null
and details_key_id is not null
)
);

View File

@@ -0,0 +1,22 @@
-- Template migration for each organization schema.
-- Replace {schema} with the real schema name, e.g. company_<organization_id>.
create table if not exists {schema}.item_price_history (
id uuid primary key,
item_id uuid not null references {schema}.items(id) on delete cascade,
purchase_price numeric(14, 4),
sales_price numeric(14, 4),
source text not null default 'manual',
valid_from timestamptz not null default now(),
created_by_user_id uuid,
created_at timestamptz not null default now(),
constraint item_price_history_purchase_price_non_negative check (
purchase_price is null or purchase_price >= 0
),
constraint item_price_history_sales_price_non_negative check (
sales_price is null or sales_price >= 0
)
);
create index if not exists idx_item_price_history_item_valid_from
on {schema}.item_price_history (item_id, valid_from desc);

View File

@@ -0,0 +1,9 @@
-- Template migration for each organization schema.
-- Replace {schema} with the real schema name, e.g. company_<organization_id>.
alter table {schema}.activities
add column if not exists activity_number text;
create unique index if not exists idx_activities_activity_number
on {schema}.activities (activity_number)
where activity_number is not null;

View File

@@ -0,0 +1,22 @@
-- Template migration for each organization schema.
-- Replace {schema} with the real schema name, e.g. company_<organization_id>.
update {schema}.number_ranges
set pattern = 'KU{counter}', updated_at = now()
where code = 'customers';
update {schema}.number_ranges
set pattern = 'LI{counter}', updated_at = now()
where code = 'suppliers';
update {schema}.number_ranges
set pattern = 'AR{counter}', updated_at = now()
where code = 'items';
update {schema}.number_ranges
set pattern = 'AK{counter}', updated_at = now()
where code = 'activities';
update {schema}.number_ranges
set pattern = 'AR{counter}', updated_at = now()
where code = 'outgoing_invoices';

View File

@@ -0,0 +1,82 @@
-- Template migration for each organization schema.
-- Replace {schema} with the real schema name, e.g. company_<organization_id>.
create table if not exists {schema}.quotes (
id uuid primary key,
quote_number text not null unique,
customer_id uuid not null references {schema}.customers(id),
status text not null default 'draft',
valid_until date,
cash_discount_term_id uuid references {schema}.cash_discount_terms(id),
customer_discount_percent numeric(7, 4) not null default 0,
notes_ciphertext bytea,
notes_nonce bytea,
notes_key_id text,
created_by_user_id uuid,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint quotes_status_valid check (
status in ('draft', 'sent', 'accepted', 'rejected', 'expired', 'cancelled')
),
constraint quotes_customer_discount_valid check (
customer_discount_percent >= 0 and customer_discount_percent <= 100
),
constraint quotes_notes_encryption_complete check (
(
notes_ciphertext is null
and notes_nonce is null
and notes_key_id is null
)
or (
notes_ciphertext is not null
and notes_nonce is not null
and notes_key_id is not null
)
)
);
create index if not exists idx_quotes_customer_status
on {schema}.quotes (customer_id, status);
create table if not exists {schema}.quote_items (
id uuid primary key,
quote_id uuid not null references {schema}.quotes(id) on delete cascade,
line_number integer not null,
item_id uuid not null references {schema}.items(id),
description_ciphertext bytea,
description_nonce bytea,
description_key_id text,
quantity numeric(14, 4) not null,
unit_price numeric(14, 4) not null,
original_unit_price numeric(14, 4),
discount_percent numeric(7, 4) not null default 0,
price_overridden boolean not null default false,
tax_rate numeric(7, 4) not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (quote_id, line_number),
constraint quote_items_quantity_positive check (quantity > 0),
constraint quote_items_unit_price_non_negative check (unit_price >= 0),
constraint quote_items_original_price_non_negative check (
original_unit_price is null or original_unit_price >= 0
),
constraint quote_items_discount_valid check (
discount_percent >= 0 and discount_percent <= 100
),
constraint quote_items_tax_rate_non_negative check (tax_rate >= 0),
constraint quote_items_description_encryption_complete check (
(
description_ciphertext is null
and description_nonce is null
and description_key_id is null
)
or (
description_ciphertext is not null
and description_nonce is not null
and description_key_id is not null
)
)
);
create index if not exists idx_quote_items_item
on {schema}.quote_items (item_id);

View File

@@ -0,0 +1,19 @@
-- Template migration for each organization schema.
-- Replace {schema} with the real schema name, e.g. company_<organization_id>.
alter table {schema}.outgoing_invoices
add column if not exists source_quote_id uuid references {schema}.quotes(id);
alter table {schema}.outgoing_invoices
add column if not exists customer_discount_percent numeric(7, 4) not null default 0;
alter table {schema}.outgoing_invoices
drop constraint if exists outgoing_invoices_customer_discount_valid;
alter table {schema}.outgoing_invoices
add constraint outgoing_invoices_customer_discount_valid check (
customer_discount_percent >= 0 and customer_discount_percent <= 100
);
create index if not exists idx_outgoing_invoices_source_quote
on {schema}.outgoing_invoices (source_quote_id);

View File

@@ -0,0 +1,71 @@
-- Template migration for each organization schema.
-- Replace {schema} with the real schema name, e.g. company_<organization_id>.
create table if not exists {schema}.imports (
id uuid primary key,
import_type text not null,
source_name text not null,
status text not null default 'previewed',
total_rows integer not null default 0,
applied_rows integer not null default 0,
error_rows integer not null default 0,
created_by_user_id uuid,
created_at timestamptz not null default now(),
finished_at timestamptz,
constraint imports_type_valid check (import_type in ('price_list', 'api_price_sync')),
constraint imports_status_valid check (status in ('previewed', 'applied', 'failed'))
);
create table if not exists {schema}.import_mappings (
id uuid primary key,
code text not null unique,
name text not null,
delimiter text not null default ';',
item_number_column text not null default 'item_number',
name_column text not null default 'name',
unit_column text not null default 'unit',
tax_rate_column text not null default 'tax_rate',
purchase_price_column text not null default 'purchase_price',
sales_price_column text not null default 'sales_price',
is_default boolean not null default false,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create unique index if not exists idx_import_mappings_default
on {schema}.import_mappings (is_default)
where is_default;
create table if not exists {schema}.price_rules (
id uuid primary key,
code text not null unique,
name text not null,
source_type text not null default 'import',
source_id uuid,
markup_percent numeric(7, 4) not null default 0,
rounding_mode text not null default 'none',
is_active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint price_rules_source_type_valid check (source_type in ('import', 'api', 'supplier')),
constraint price_rules_markup_valid check (markup_percent >= -100 and markup_percent <= 1000),
constraint price_rules_rounding_mode_valid check (rounding_mode in ('none', 'cent', 'five_cent', 'ten_cent', 'whole'))
);
create table if not exists {schema}.api_connectors (
id uuid primary key,
code text not null unique,
name text not null,
connector_type text not null,
config_ciphertext bytea not null,
config_nonce bytea not null,
config_key_id text not null,
is_active boolean not null default true,
sync_interval_minutes integer,
last_sync_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint api_connectors_interval_valid check (
sync_interval_minutes is null or sync_interval_minutes > 0
)
);

View File

@@ -0,0 +1,153 @@
-- Template migration for each organization schema.
-- Replace {schema} with the real schema name, e.g. company_<organization_id>.
create table if not exists {schema}.communications (
id uuid primary key,
communication_type text not null,
direction text not null,
subject_ciphertext bytea not null,
subject_nonce bytea not null,
subject_key_id text not null,
body_ciphertext bytea,
body_nonce bytea,
body_key_id text,
status text not null default 'open',
occurred_at timestamptz,
created_by_user_id uuid,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint communications_type_valid check (
communication_type in ('email', 'phone', 'letter', 'meeting', 'internal_note')
),
constraint communications_direction_valid check (
direction in ('inbound', 'outbound', 'internal')
),
constraint communications_status_valid check (
status in ('open', 'done', 'archived')
),
constraint communications_body_encryption_complete check (
(
body_ciphertext is null
and body_nonce is null
and body_key_id is null
)
or (
body_ciphertext is not null
and body_nonce is not null
and body_key_id is not null
)
)
);
create index if not exists idx_communications_type_status
on {schema}.communications (communication_type, status, occurred_at desc);
create table if not exists {schema}.communication_links (
communication_id uuid not null references {schema}.communications(id) on delete cascade,
entity_type text not null,
entity_id uuid not null,
created_at timestamptz not null default now(),
primary key (communication_id, entity_type, entity_id),
constraint communication_links_entity_type_valid check (
entity_type in (
'customer',
'supplier',
'activity',
'quote',
'outgoing_invoice',
'incoming_invoice',
'item',
'document'
)
)
);
create index if not exists idx_communication_links_entity
on {schema}.communication_links (entity_type, entity_id);
create table if not exists {schema}.documents (
id uuid primary key,
title_ciphertext bytea not null,
title_nonce bytea not null,
title_key_id text not null,
description_ciphertext bytea,
description_nonce bytea,
description_key_id text,
status text not null default 'active',
created_by_user_id uuid,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint documents_status_valid check (status in ('active', 'archived', 'deleted')),
constraint documents_description_encryption_complete check (
(
description_ciphertext is null
and description_nonce is null
and description_key_id is null
)
or (
description_ciphertext is not null
and description_nonce is not null
and description_key_id is not null
)
)
);
create table if not exists {schema}.document_versions (
id uuid primary key,
document_id uuid not null references {schema}.documents(id) on delete cascade,
version_no integer not null,
file_name_ciphertext bytea not null,
file_name_nonce bytea not null,
file_name_key_id text not null,
content_type_ciphertext bytea not null,
content_type_nonce bytea not null,
content_type_key_id text not null,
file_size bigint not null,
storage_path text not null,
checksum_sha256 text not null,
uploaded_by_user_id uuid,
created_at timestamptz not null default now(),
unique (document_id, version_no),
constraint document_versions_file_size_valid check (file_size >= 0)
);
create index if not exists idx_document_versions_document_version
on {schema}.document_versions (document_id, version_no desc);
create table if not exists {schema}.document_links (
document_id uuid not null references {schema}.documents(id) on delete cascade,
entity_type text not null,
entity_id uuid not null,
created_at timestamptz not null default now(),
primary key (document_id, entity_type, entity_id),
constraint document_links_entity_type_valid check (
entity_type in (
'customer',
'supplier',
'activity',
'communication',
'quote',
'outgoing_invoice',
'incoming_invoice',
'item'
)
)
);
create index if not exists idx_document_links_entity
on {schema}.document_links (entity_type, entity_id);
create table if not exists {schema}.document_audit_log (
id uuid primary key,
document_id uuid not null references {schema}.documents(id) on delete cascade,
version_id uuid references {schema}.document_versions(id) on delete set null,
action text not null,
user_id uuid,
created_at timestamptz not null default now(),
constraint document_audit_log_action_valid check (
action in ('upload', 'download', 'archive')
)
);
create index if not exists idx_document_audit_log_document
on {schema}.document_audit_log (document_id, created_at desc);

View File

@@ -0,0 +1,9 @@
create table if not exists {schema}.user_settings (
user_id uuid not null,
key text not null,
value_ciphertext bytea not null,
value_nonce bytea not null,
value_key_id text not null,
updated_at timestamptz not null default now(),
primary key (user_id, key)
);

View File

@@ -0,0 +1,28 @@
alter table {schema}.items
add column if not exists manufacturer_code text;
create table if not exists {schema}.item_supplier_prices (
id uuid primary key,
item_id uuid not null references {schema}.items(id) on delete cascade,
supplier_id uuid not null references {schema}.suppliers(id) on delete cascade,
external_item_number text not null,
purchase_price numeric(14, 4) not null,
currency text not null default 'EUR',
is_preferred boolean not null default false,
valid_from date,
valid_until date,
source text not null default 'manual',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint item_supplier_prices_purchase_price_non_negative check (purchase_price >= 0),
constraint item_supplier_prices_currency_valid check (char_length(currency) = 3),
constraint item_supplier_prices_valid_range check (
valid_until is null or valid_from is null or valid_until >= valid_from
)
);
create unique index if not exists idx_item_supplier_prices_supplier_external
on {schema}.item_supplier_prices (supplier_id, external_item_number);
create index if not exists idx_item_supplier_prices_item
on {schema}.item_supplier_prices (item_id, purchase_price);