diff --git a/index.html b/index.html
index 28613ec..4024a67 100644
--- a/index.html
+++ b/index.html
@@ -19,7 +19,7 @@
-
BOHA | Admin
+ Admin
diff --git a/package-lock.json b/package-lock.json
index 29448e7..1a683df 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "boha-app-ts",
+ "name": "app-ts",
"version": "1.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "boha-app-ts",
+ "name": "app-ts",
"version": "1.1.4",
"license": "ISC",
"dependencies": {
@@ -28,6 +28,7 @@
"framer-motion": "^12.38.0",
"hi-base32": "^0.5.1",
"jsonwebtoken": "^9.0.3",
+ "leaflet": "^1.9.4",
"node-cron": "^4.2.1",
"nodemailer": "^8.0.2",
"otpauth": "^9.5.0",
@@ -45,6 +46,7 @@
"@types/bcryptjs": "^2.4.6",
"@types/dompurify": "^3.0.5",
"@types/jsonwebtoken": "^9.0.10",
+ "@types/leaflet": "^1.9.21",
"@types/mysql": "^2.15.27",
"@types/node": "^25.5.0",
"@types/node-cron": "^3.0.11",
@@ -1504,6 +1506,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
@@ -1515,6 +1524,16 @@
"@types/node": "*"
}
},
+ "node_modules/@types/leaflet": {
+ "version": "1.9.21",
+ "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
+ "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
"node_modules/@types/methods": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@@ -3699,6 +3718,12 @@
"safe-buffer": "^5.0.1"
}
},
+ "node_modules/leaflet": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
+ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/light-my-request": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz",
diff --git a/package.json b/package.json
index 3434908..02aaab8 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"framer-motion": "^12.38.0",
"hi-base32": "^0.5.1",
"jsonwebtoken": "^9.0.3",
+ "leaflet": "^1.9.4",
"node-cron": "^4.2.1",
"nodemailer": "^8.0.2",
"otpauth": "^9.5.0",
@@ -60,6 +61,7 @@
"@types/bcryptjs": "^2.4.6",
"@types/dompurify": "^3.0.5",
"@types/jsonwebtoken": "^9.0.10",
+ "@types/leaflet": "^1.9.21",
"@types/mysql": "^2.15.27",
"@types/node": "^25.5.0",
"@types/node-cron": "^3.0.11",
diff --git a/prisma/migrations/20260327_add_dark_logo/migration.sql b/prisma/migrations/20260327_add_dark_logo/migration.sql
new file mode 100644
index 0000000..ff13d73
--- /dev/null
+++ b/prisma/migrations/20260327_add_dark_logo/migration.sql
@@ -0,0 +1 @@
+ALTER TABLE `company_settings` ADD COLUMN `logo_data_dark` MEDIUMBLOB NULL AFTER `logo_data`;
diff --git a/prisma/migrations/20260327_add_number_patterns/migration.sql b/prisma/migrations/20260327_add_number_patterns/migration.sql
new file mode 100644
index 0000000..95e178b
--- /dev/null
+++ b/prisma/migrations/20260327_add_number_patterns/migration.sql
@@ -0,0 +1,4 @@
+ALTER TABLE `company_settings`
+ ADD COLUMN `offer_number_pattern` VARCHAR(100) NULL,
+ ADD COLUMN `order_number_pattern` VARCHAR(100) NULL,
+ ADD COLUMN `invoice_number_pattern` VARCHAR(100) NULL;
diff --git a/prisma/migrations/20260327_add_smtp_settings/migration.sql b/prisma/migrations/20260327_add_smtp_settings/migration.sql
new file mode 100644
index 0000000..900f806
--- /dev/null
+++ b/prisma/migrations/20260327_add_smtp_settings/migration.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `company_settings`
+ ADD COLUMN `smtp_from` VARCHAR(255) NULL,
+ ADD COLUMN `smtp_from_name` VARCHAR(255) NULL;
diff --git a/prisma/migrations/20260327_add_system_settings/migration.sql b/prisma/migrations/20260327_add_system_settings/migration.sql
new file mode 100644
index 0000000..74412f4
--- /dev/null
+++ b/prisma/migrations/20260327_add_system_settings/migration.sql
@@ -0,0 +1,13 @@
+-- System settings columns on company_settings
+ALTER TABLE `company_settings`
+ ADD COLUMN `break_threshold_hours` DECIMAL(4,2) DEFAULT 6,
+ ADD COLUMN `break_duration_short` INT DEFAULT 15,
+ ADD COLUMN `break_duration_long` INT DEFAULT 30,
+ ADD COLUMN `clock_rounding_minutes` INT DEFAULT 15,
+ ADD COLUMN `invoice_alert_email` VARCHAR(255) NULL,
+ ADD COLUMN `leave_notify_email` VARCHAR(255) NULL,
+ ADD COLUMN `max_login_attempts` INT DEFAULT 5,
+ ADD COLUMN `lockout_minutes` INT DEFAULT 15,
+ ADD COLUMN `max_requests_per_minute` INT DEFAULT 300,
+ ADD COLUMN `available_vat_rates` LONGTEXT NULL,
+ ADD COLUMN `available_currencies` LONGTEXT NULL;
diff --git a/prisma/migrations/20260327_consolidate_settings_permissions/migration.sql b/prisma/migrations/20260327_consolidate_settings_permissions/migration.sql
new file mode 100644
index 0000000..9019c22
--- /dev/null
+++ b/prisma/migrations/20260327_consolidate_settings_permissions/migration.sql
@@ -0,0 +1,18 @@
+-- Create new unified permission
+INSERT INTO permissions (name, display_name, description, module)
+VALUES ('settings.manage', 'Správa nastavení', 'Správa všech nastavení systému', 'settings')
+ON DUPLICATE KEY UPDATE display_name = VALUES(display_name);
+
+-- Grant to all roles that had any of the old 3
+INSERT IGNORE INTO role_permissions (role_id, permission_id)
+SELECT DISTINCT rp.role_id, (SELECT id FROM permissions WHERE name = 'settings.manage')
+FROM role_permissions rp
+JOIN permissions p ON p.id = rp.permission_id
+WHERE p.name IN ('offers.settings', 'settings.roles', 'settings.security');
+
+-- Clean up old role_permissions
+DELETE FROM role_permissions
+WHERE permission_id IN (SELECT id FROM permissions WHERE name IN ('offers.settings', 'settings.roles', 'settings.security'));
+
+-- Remove old permissions
+DELETE FROM permissions WHERE name IN ('offers.settings', 'settings.roles', 'settings.security');
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 6d60854..49995ea 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -100,6 +100,7 @@ model company_settings {
vat_id String? @db.VarChar(50)
custom_fields String? @db.LongText
logo_data Bytes?
+ logo_data_dark Bytes?
quotation_prefix String? @db.VarChar(20)
default_currency String? @default("CZK") @db.VarChar(10)
default_vat_rate Decimal? @default(21.00) @db.Decimal(5, 2)
@@ -109,7 +110,23 @@ model company_settings {
sync_version Int? @default(0)
order_type_code String? @db.VarChar(10)
invoice_type_code String? @db.VarChar(10)
- require_2fa Boolean @default(false)
+ require_2fa Boolean @default(false)
+ break_threshold_hours Decimal? @default(6) @db.Decimal(4, 2)
+ break_duration_short Int? @default(15)
+ break_duration_long Int? @default(30)
+ clock_rounding_minutes Int? @default(15)
+ invoice_alert_email String? @db.VarChar(255)
+ leave_notify_email String? @db.VarChar(255)
+ max_login_attempts Int? @default(5)
+ lockout_minutes Int? @default(15)
+ max_requests_per_minute Int? @default(300)
+ available_vat_rates String? @db.LongText
+ available_currencies String? @db.LongText
+ smtp_from String? @db.VarChar(255)
+ smtp_from_name String? @db.VarChar(255)
+ offer_number_pattern String? @db.VarChar(100)
+ order_number_pattern String? @db.VarChar(100)
+ invoice_number_pattern String? @db.VarChar(100)
}
model customers {
diff --git a/src/admin/AdminApp.tsx b/src/admin/AdminApp.tsx
index 5913425..6633640 100644
--- a/src/admin/AdminApp.tsx
+++ b/src/admin/AdminApp.tsx
@@ -32,7 +32,6 @@ const Offers = lazy(() => import("./pages/Offers"));
const OfferDetail = lazy(() => import("./pages/OfferDetail"));
const OffersCustomers = lazy(() => import("./pages/OffersCustomers"));
const OffersTemplates = lazy(() => import("./pages/OffersTemplates"));
-const CompanySettings = lazy(() => import("./pages/CompanySettings"));
const Orders = lazy(() => import("./pages/Orders"));
const OrderDetail = lazy(() => import("./pages/OrderDetail"));
const Projects = lazy(() => import("./pages/Projects"));
@@ -91,7 +90,6 @@ export default function AdminApp() {
} />
} />
} />
- } />
} />
} />
} />
diff --git a/src/admin/components/Sidebar.tsx b/src/admin/components/Sidebar.tsx
index dde6e87..aa9f80e 100644
--- a/src/admin/components/Sidebar.tsx
+++ b/src/admin/components/Sidebar.tsx
@@ -330,22 +330,6 @@ const menuSections: MenuSection[] = [
),
},
- {
- path: "/company/settings",
- label: "Firma",
- permission: "offers.settings",
- icon: (
-
- ),
- },
],
},
{
@@ -372,7 +356,7 @@ const menuSections: MenuSection[] = [
{
path: "/settings",
label: "Nastavení",
- permission: ["settings.roles", "settings.security"],
+ permission: "settings.manage",
icon: (