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:
80
backend/company-migrations/0001_company_base.sql
Normal file
80
backend/company-migrations/0001_company_base.sql
Normal 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
|
||||
);
|
||||
360
backend/company-migrations/0002_activity_price_invoice_rules.sql
Normal file
360
backend/company-migrations/0002_activity_price_invoice_rules.sql
Normal 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
|
||||
)
|
||||
)
|
||||
);
|
||||
45
backend/company-migrations/0003_customer_details.sql
Normal file
45
backend/company-migrations/0003_customer_details.sql
Normal 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
|
||||
)
|
||||
);
|
||||
22
backend/company-migrations/0004_item_price_history.sql
Normal file
22
backend/company-migrations/0004_item_price_history.sql
Normal 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);
|
||||
9
backend/company-migrations/0005_numbered_activities.sql
Normal file
9
backend/company-migrations/0005_numbered_activities.sql
Normal 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;
|
||||
@@ -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';
|
||||
82
backend/company-migrations/0007_quotes.sql
Normal file
82
backend/company-migrations/0007_quotes.sql
Normal 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);
|
||||
19
backend/company-migrations/0008_invoice_links.sql
Normal file
19
backend/company-migrations/0008_invoice_links.sql
Normal 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);
|
||||
71
backend/company-migrations/0009_price_imports.sql
Normal file
71
backend/company-migrations/0009_price_imports.sql
Normal 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
|
||||
)
|
||||
);
|
||||
153
backend/company-migrations/0010_communications_documents.sql
Normal file
153
backend/company-migrations/0010_communications_documents.sql
Normal 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);
|
||||
9
backend/company-migrations/0011_user_settings.sql
Normal file
9
backend/company-migrations/0011_user_settings.sql
Normal 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)
|
||||
);
|
||||
28
backend/company-migrations/0012_item_supplier_prices.sql
Normal file
28
backend/company-migrations/0012_item_supplier_prices.sql
Normal 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);
|
||||
Reference in New Issue
Block a user