Füge Spalte product_quality zur Tabelle stock hinzu und erstelle Migration für weather_type_id in production
This commit is contained in:
@@ -8,29 +8,87 @@ module.exports = {
|
|||||||
ADD COLUMN IF NOT EXISTS character_name text;
|
ADD COLUMN IF NOT EXISTS character_name text;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 2) Create helper function to populate character_name from character_id
|
// 1b) Add character_id column so triggers and application can set a reference
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_log.notification
|
||||||
|
ADD COLUMN IF NOT EXISTS character_id integer;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create an index on character_id to speed lookups (if not exists)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relkind = 'i' AND c.relname = 'idx_notification_character_id' AND n.nspname = 'falukant_log'
|
||||||
|
) THEN
|
||||||
|
CREATE INDEX idx_notification_character_id ON falukant_log.notification (character_id);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 2) Create helper function to populate character_name from character_id or user_id
|
||||||
|
// - Resolve name via character_id if present
|
||||||
|
// - Fallback to a character for the same user_id when character_id is NULL
|
||||||
|
// - Only set NEW.character_name when the column exists and is NULL
|
||||||
await queryInterface.sequelize.query(`
|
await queryInterface.sequelize.query(`
|
||||||
CREATE OR REPLACE FUNCTION falukant_log.populate_notification_character_name()
|
CREATE OR REPLACE FUNCTION falukant_log.populate_notification_character_name()
|
||||||
RETURNS TRIGGER AS $function$
|
RETURNS TRIGGER AS $function$
|
||||||
DECLARE
|
DECLARE
|
||||||
v_first_name TEXT;
|
v_first_name TEXT;
|
||||||
v_last_name TEXT;
|
v_last_name TEXT;
|
||||||
|
v_char_id INTEGER;
|
||||||
|
v_column_exists BOOLEAN;
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NEW.character_name IS NULL AND NEW.character_id IS NOT NULL THEN
|
-- check if target column exists in the notification table
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'falukant_log' AND table_name = 'notification' AND column_name = 'character_name'
|
||||||
|
) INTO v_column_exists;
|
||||||
|
|
||||||
|
IF NOT v_column_exists THEN
|
||||||
|
-- Nothing to do when target column absent
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- only populate when column is NULL
|
||||||
|
IF NEW.character_name IS NOT NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- prefer explicit character_id
|
||||||
|
v_char_id := NEW.character_id;
|
||||||
|
|
||||||
|
-- when character_id is null, try to find a character for the user_id
|
||||||
|
IF v_char_id IS NULL AND NEW.user_id IS NOT NULL THEN
|
||||||
|
-- choose a representative character: the one with highest id for this user (change if different policy required)
|
||||||
|
SELECT id INTO v_char_id
|
||||||
|
FROM falukant_data.character
|
||||||
|
WHERE user_id = NEW.user_id
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_char_id IS NOT NULL THEN
|
||||||
SELECT pf.name, pl.name
|
SELECT pf.name, pl.name
|
||||||
INTO v_first_name, v_last_name
|
INTO v_first_name, v_last_name
|
||||||
FROM falukant_data.character c
|
FROM falukant_data.character c
|
||||||
LEFT JOIN falukant_predefine.firstname pf ON pf.id = c.first_name
|
LEFT JOIN falukant_predefine.firstname pf ON pf.id = c.first_name
|
||||||
LEFT JOIN falukant_predefine.lastname pl ON pl.id = c.last_name
|
LEFT JOIN falukant_predefine.lastname pl ON pl.id = c.last_name
|
||||||
WHERE c.id = NEW.character_id;
|
WHERE c.id = v_char_id;
|
||||||
|
|
||||||
IF v_first_name IS NOT NULL OR v_last_name IS NOT NULL THEN
|
IF v_first_name IS NOT NULL OR v_last_name IS NOT NULL THEN
|
||||||
NEW.character_name := COALESCE(v_first_name, '') || CASE WHEN v_first_name IS NOT NULL AND v_last_name IS NOT NULL THEN ' ' ELSE '' END || COALESCE(v_last_name, '');
|
NEW.character_name := COALESCE(v_first_name, '') || CASE WHEN v_first_name IS NOT NULL AND v_last_name IS NOT NULL THEN ' ' ELSE '' END || COALESCE(v_last_name, '');
|
||||||
ELSE
|
ELSE
|
||||||
-- Fallback to placeholder with id
|
NEW.character_name := ('#' || v_char_id::text);
|
||||||
NEW.character_name := ('#' || NEW.character_id::text);
|
END IF;
|
||||||
|
ELSE
|
||||||
|
-- last resort fallback: use user_id as identifier if present
|
||||||
|
IF NEW.user_id IS NOT NULL THEN
|
||||||
|
NEW.character_name := ('#u' || NEW.user_id::text);
|
||||||
END IF;
|
END IF;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
$function$ LANGUAGE plpgsql;
|
$function$ LANGUAGE plpgsql;
|
||||||
@@ -53,9 +111,25 @@ module.exports = {
|
|||||||
await queryInterface.sequelize.query(`
|
await queryInterface.sequelize.query(`
|
||||||
DROP FUNCTION IF EXISTS falukant_log.populate_notification_character_name();
|
DROP FUNCTION IF EXISTS falukant_log.populate_notification_character_name();
|
||||||
`);
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
-- drop index if exists
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relkind = 'i' AND c.relname = 'idx_notification_character_id' AND n.nspname = 'falukant_log'
|
||||||
|
) THEN
|
||||||
|
EXECUTE 'DROP INDEX falukant_log.idx_notification_character_id';
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
`);
|
||||||
await queryInterface.sequelize.query(`
|
await queryInterface.sequelize.query(`
|
||||||
ALTER TABLE IF EXISTS falukant_log.notification
|
ALTER TABLE IF EXISTS falukant_log.notification
|
||||||
DROP COLUMN IF EXISTS character_name;
|
DROP COLUMN IF EXISTS character_name;
|
||||||
`);
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_log.notification
|
||||||
|
DROP COLUMN IF EXISTS character_id;
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
// Add nullable weather_type_id column
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_data.production
|
||||||
|
ADD COLUMN IF NOT EXISTS weather_type_id integer;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Add foreign key constraint if not exists
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.table_constraints tc
|
||||||
|
JOIN information_schema.key_column_usage kcu ON kcu.constraint_name = tc.constraint_name AND kcu.constraint_schema = tc.constraint_schema
|
||||||
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||||
|
AND tc.constraint_schema = 'falukant_data'
|
||||||
|
AND tc.table_name = 'production'
|
||||||
|
AND kcu.column_name = 'weather_type_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE falukant_data.production
|
||||||
|
ADD CONSTRAINT fk_production_weather_type
|
||||||
|
FOREIGN KEY (weather_type_id) REFERENCES falukant_type.weather(id);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// create index to speed lookups
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relkind = 'i' AND c.relname = 'idx_production_weather_type_id' AND n.nspname = 'falukant_data'
|
||||||
|
) THEN
|
||||||
|
CREATE INDEX idx_production_weather_type_id ON falukant_data.production (weather_type_id);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_data.production
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_production_weather_type;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relkind = 'i' AND c.relname = 'idx_production_weather_type_id' AND n.nspname = 'falukant_data'
|
||||||
|
) THEN
|
||||||
|
EXECUTE 'DROP INDEX falukant_data.idx_production_weather_type_id';
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_data.production
|
||||||
|
DROP COLUMN IF EXISTS weather_type_id;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_data.stock
|
||||||
|
ADD COLUMN IF NOT EXISTS product_quality integer;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_data.stock
|
||||||
|
DROP COLUMN IF EXISTS product_quality;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -14,7 +14,13 @@ FalukantStock.init({
|
|||||||
allowNull: false},
|
allowNull: false},
|
||||||
quantity: {
|
quantity: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false}}, {
|
allowNull: false},
|
||||||
|
productQuality: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Quality of the stored product (0-100)'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'StockData',
|
modelName: 'StockData',
|
||||||
tableName: 'stock',
|
tableName: 'stock',
|
||||||
|
|||||||
@@ -133,6 +133,64 @@ async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPe
|
|||||||
return parseFloat(val) || 0;
|
return parseFloat(val) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns cumulative tax percent for a region, but excludes regions where the user holds
|
||||||
|
// a political office that grants tax exemption according to the rules.
|
||||||
|
// exemptionsMap maps political office.name -> array of regionType labelTr that are exempted
|
||||||
|
const POLITICAL_TAX_EXEMPTIONS = {
|
||||||
|
'council': ['city'],
|
||||||
|
'taxman': ['city', 'county'],
|
||||||
|
'treasurerer': ['city', 'county', 'shire'],
|
||||||
|
'super-state-administrator': ['city', 'county', 'shire', 'markgrave', 'duchy'],
|
||||||
|
'chancellor': ['city','county','shire','markgrave','duchy','duchy'] // chancellor = all types; we'll handle as wildcard
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getCumulativeTaxPercentWithExemptions(userId, regionId) {
|
||||||
|
if (!regionId) return 0;
|
||||||
|
// fetch user's political offices (active) and their region types
|
||||||
|
const offices = await PoliticalOffice.findAll({
|
||||||
|
where: { userId },
|
||||||
|
include: [{ model: PoliticalOfficeType, as: 'type', attributes: ['name'] }, { model: RegionData, as: 'region', include: [{ model: RegionType, as: 'regionType', attributes: ['labelTr'] }] }]
|
||||||
|
});
|
||||||
|
|
||||||
|
// build set of exempt region type labels from user's offices
|
||||||
|
const exemptTypes = new Set();
|
||||||
|
let hasChancellor = false;
|
||||||
|
for (const o of offices) {
|
||||||
|
const name = o.type?.name;
|
||||||
|
if (!name) continue;
|
||||||
|
if (name === 'chancellor') { hasChancellor = true; break; }
|
||||||
|
const allowed = POLITICAL_TAX_EXEMPTIONS[name];
|
||||||
|
if (allowed && Array.isArray(allowed)) {
|
||||||
|
for (const t of allowed) exemptTypes.add(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If chancellor, exempt all region types -> tax = 0
|
||||||
|
if (hasChancellor) return 0;
|
||||||
|
|
||||||
|
// Now compute cumulative tax but exclude regions whose regionType.labelTr is in exemptTypes
|
||||||
|
const rows = await sequelize.query(
|
||||||
|
`WITH RECURSIVE ancestors AS (
|
||||||
|
SELECT r.id, r.parent_id, r.tax_percent, rt.label_tr as region_type
|
||||||
|
FROM falukant_data.region r
|
||||||
|
JOIN falukant_type.region_type rt ON rt.id = r.region_type_id
|
||||||
|
WHERE r.id = :id
|
||||||
|
UNION ALL
|
||||||
|
SELECT reg.id, reg.parent_id, reg.tax_percent, rt2.label_tr
|
||||||
|
FROM falukant_data.region reg
|
||||||
|
JOIN falukant_type.region_type rt2 ON rt2.id = reg.region_type_id
|
||||||
|
JOIN ancestors a ON reg.id = a.parent_id
|
||||||
|
)
|
||||||
|
SELECT COALESCE(SUM(CASE WHEN :exempt_types::text[] && ARRAY[region_type] THEN 0 ELSE tax_percent END),0) AS total FROM ancestors;`,
|
||||||
|
{
|
||||||
|
replacements: { id: regionId, exempt_types: Array.from(exemptTypes) },
|
||||||
|
type: sequelize.QueryTypes.SELECT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const val = rows?.[0]?.total ?? 0;
|
||||||
|
return parseFloat(val) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
function calculateMarriageCost(titleOfNobility, age) {
|
function calculateMarriageCost(titleOfNobility, age) {
|
||||||
const minTitle = 1;
|
const minTitle = 1;
|
||||||
const adjustedTitle = titleOfNobility - minTitle + 1;
|
const adjustedTitle = titleOfNobility - minTitle + 1;
|
||||||
@@ -1527,8 +1585,8 @@ class FalukantService extends BaseService {
|
|||||||
const knowledgeVal = item.knowledges?.[0]?.knowledge || 0;
|
const knowledgeVal = item.knowledges?.[0]?.knowledge || 0;
|
||||||
const pricePerUnit = await calcRegionalSellPrice(item, knowledgeVal, branch.regionId);
|
const pricePerUnit = await calcRegionalSellPrice(item, knowledgeVal, branch.regionId);
|
||||||
|
|
||||||
// compute cumulative tax (region + ancestors) and inflate price so seller net is unchanged
|
// compute cumulative tax (region + ancestors) with political exemptions and inflate price so seller net is unchanged
|
||||||
const cumulativeTax = await getCumulativeTaxPercent(branch.regionId);
|
const cumulativeTax = await getCumulativeTaxPercentWithExemptions(user.id, branch.regionId);
|
||||||
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
|
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
|
||||||
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
|
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
|
||||||
const revenue = quantity * adjustedPricePerUnit;
|
const revenue = quantity * adjustedPricePerUnit;
|
||||||
@@ -1614,7 +1672,7 @@ class FalukantService extends BaseService {
|
|||||||
const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0;
|
const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0;
|
||||||
const regionId = item.stock.branch.regionId;
|
const regionId = item.stock.branch.regionId;
|
||||||
const pricePerUnit = await calcRegionalSellPrice(item.productType, knowledgeVal, regionId);
|
const pricePerUnit = await calcRegionalSellPrice(item.productType, knowledgeVal, regionId);
|
||||||
const cumulativeTax = await getCumulativeTaxPercent(regionId);
|
const cumulativeTax = await getCumulativeTaxPercentWithExemptions(user.id, regionId);
|
||||||
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
|
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
|
||||||
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
|
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
|
||||||
total += item.quantity * adjustedPricePerUnit;
|
total += item.quantity * adjustedPricePerUnit;
|
||||||
@@ -4363,15 +4421,26 @@ class FalukantService extends BaseService {
|
|||||||
|
|
||||||
// Unikate nach character.id
|
// Unikate nach character.id
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
|
const POLITICAL_TAX_EXEMPTIONS = {
|
||||||
|
'council': ['city'],
|
||||||
|
'taxman': ['city','county'],
|
||||||
|
'treasurerer': ['city','county','shire'],
|
||||||
|
'super-state-administrator': ['city','county','shire','markgrave','duchy'],
|
||||||
|
'chancellor': ['*']
|
||||||
|
};
|
||||||
|
|
||||||
histories.forEach(h => {
|
histories.forEach(h => {
|
||||||
const c = h.holder;
|
const c = h.holder;
|
||||||
if (c && c.id && !map.has(c.id)) {
|
if (c && c.id && !map.has(c.id)) {
|
||||||
|
const officeName = h.type?.name;
|
||||||
|
const benefit = POLITICAL_TAX_EXEMPTIONS[officeName] || [];
|
||||||
map.set(c.id, {
|
map.set(c.id, {
|
||||||
id: c.id,
|
id: c.id,
|
||||||
name: `${c.definedFirstName.name} ${c.definedLastName.name}`,
|
name: `${c.definedFirstName.name} ${c.definedLastName.name}`,
|
||||||
title: c.nobleTitle.labelTr,
|
title: c.nobleTitle.labelTr,
|
||||||
officeType: h.type.name,
|
officeType: officeName,
|
||||||
gender: c.gender
|
gender: c.gender,
|
||||||
|
benefit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
11
backend/sql/add_product_quality_to_stock.sql
Normal file
11
backend/sql/add_product_quality_to_stock.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- Migration script: add_product_quality_to_stock.sql
|
||||||
|
-- Fügt die Spalte product_quality zur Tabelle falukant_data.stock hinzu (nullable, idempotent)
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS falukant_data.stock
|
||||||
|
ADD COLUMN IF NOT EXISTS product_quality integer;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- Ende
|
||||||
38
backend/sql/add_weather_type_to_production.sql
Normal file
38
backend/sql/add_weather_type_to_production.sql
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
-- Migration script: add_weather_type_to_production.sql
|
||||||
|
-- Legt die Spalte weather_type_id in falukant_data.production an,
|
||||||
|
-- fügt optional einen Foreign Key zu falukant_type.weather(id) hinzu
|
||||||
|
-- und erstellt einen Index. Idempotent (mehrfaches Ausführen ist unproblematisch).
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- 1) Spalte anlegen (nullable, idempotent)
|
||||||
|
ALTER TABLE IF EXISTS falukant_data.production
|
||||||
|
ADD COLUMN IF NOT EXISTS weather_type_id integer;
|
||||||
|
|
||||||
|
-- 2) Fremdschlüssel nur hinzufügen, falls noch kein FK für diese Spalte existiert
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.table_constraints tc
|
||||||
|
JOIN information_schema.key_column_usage kcu
|
||||||
|
ON kcu.constraint_name = tc.constraint_name
|
||||||
|
AND kcu.constraint_schema = tc.constraint_schema
|
||||||
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||||
|
AND tc.constraint_schema = 'falukant_data'
|
||||||
|
AND tc.table_name = 'production'
|
||||||
|
AND kcu.column_name = 'weather_type_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE falukant_data.production
|
||||||
|
ADD CONSTRAINT fk_production_weather_type
|
||||||
|
FOREIGN KEY (weather_type_id) REFERENCES falukant_type.weather(id);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
|
||||||
|
-- 3) Index (Postgres: CREATE INDEX IF NOT EXISTS)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_production_weather_type_id
|
||||||
|
ON falukant_data.production (weather_type_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- Ende
|
||||||
@@ -5,7 +5,10 @@
|
|||||||
:title="'falukant.messages.title'"
|
:title="'falukant.messages.title'"
|
||||||
:isTitleTranslated="true"
|
:isTitleTranslated="true"
|
||||||
icon="falukant/messages24.png"
|
icon="falukant/messages24.png"
|
||||||
:buttons="[{ text: 'message.close', action: 'close' }]"
|
:buttons="[
|
||||||
|
{ text: 'falukant.messages.markAllRead', action: 'markAll' },
|
||||||
|
{ text: 'message.close', action: 'close' }
|
||||||
|
]"
|
||||||
width="520px"
|
width="520px"
|
||||||
height="420px"
|
height="420px"
|
||||||
>
|
>
|
||||||
@@ -59,6 +62,15 @@ export default {
|
|||||||
// mark unread as shown
|
// mark unread as shown
|
||||||
try { await apiClient.post('/api/falukant/notifications/mark-shown'); } catch {}
|
try { await apiClient.post('/api/falukant/notifications/mark-shown'); } catch {}
|
||||||
},
|
},
|
||||||
|
async markAll() {
|
||||||
|
try {
|
||||||
|
await apiClient.post('/api/falukant/notifications/mark-shown');
|
||||||
|
// reload to update shown flags and unread count
|
||||||
|
await this.load();
|
||||||
|
} catch (e) {
|
||||||
|
// ignore errors silently
|
||||||
|
}
|
||||||
|
},
|
||||||
async load() {
|
async load() {
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.get('/api/falukant/notifications/all', { params: { page: this.page, size: this.size } });
|
const { data } = await apiClient.get('/api/falukant/notifications/all', { params: { page: this.page, size: this.size } });
|
||||||
|
|||||||
@@ -27,7 +27,8 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"title": "Nachrichten",
|
"title": "Nachrichten",
|
||||||
"tooltip": "Nachrichten",
|
"tooltip": "Nachrichten",
|
||||||
"empty": "Keine Nachrichten vorhanden."
|
"empty": "Keine Nachrichten vorhanden.",
|
||||||
|
"markAllRead": "Alle als gelesen markieren"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"notify_election_created": "Es wurde eine neue Wahl ausgeschrieben.",
|
"notify_election_created": "Es wurde eine neue Wahl ausgeschrieben.",
|
||||||
@@ -902,6 +903,8 @@
|
|||||||
"office": "Amt",
|
"office": "Amt",
|
||||||
"region": "Region",
|
"region": "Region",
|
||||||
"termEnds": "Läuft ab am",
|
"termEnds": "Läuft ab am",
|
||||||
|
"benefit": "Vorteil",
|
||||||
|
"benefit_all": "Alle Regionen",
|
||||||
"income": "Einkommen",
|
"income": "Einkommen",
|
||||||
"none": "Keine aktuelle Position vorhanden.",
|
"none": "Keine aktuelle Position vorhanden.",
|
||||||
"holder": "Inhaber"
|
"holder": "Inhaber"
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"title": "Messages",
|
"title": "Messages",
|
||||||
"tooltip": "Messages",
|
"tooltip": "Messages",
|
||||||
"empty": "No messages."
|
"empty": "No messages.",
|
||||||
|
"markAllRead": "Mark all as read"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"notify_election_created": "A new election has been scheduled.",
|
"notify_election_created": "A new election has been scheduled.",
|
||||||
@@ -211,6 +212,8 @@
|
|||||||
"office": "Office",
|
"office": "Office",
|
||||||
"region": "Region",
|
"region": "Region",
|
||||||
"termEnds": "Term Ends",
|
"termEnds": "Term Ends",
|
||||||
|
"benefit": "Benefit",
|
||||||
|
"benefit_all": "All regions",
|
||||||
"income": "Income",
|
"income": "Income",
|
||||||
"none": "No current position available.",
|
"none": "No current position available.",
|
||||||
"holder": "Holder"
|
"holder": "Holder"
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>{{ $t('falukant.politics.current.office') }}</th>
|
<th>{{ $t('falukant.politics.current.office') }}</th>
|
||||||
<th>{{ $t('falukant.politics.current.region') }}</th>
|
<th>{{ $t('falukant.politics.current.region') }}</th>
|
||||||
<th>{{ $t('falukant.politics.current.holder') }}</th>
|
<th>{{ $t('falukant.politics.current.holder') }}</th>
|
||||||
|
<th>{{ $t('falukant.politics.current.benefit') }}</th>
|
||||||
<th>{{ $t('falukant.politics.current.termEnds') }}</th>
|
<th>{{ $t('falukant.politics.current.termEnds') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -32,6 +33,13 @@
|
|||||||
</span>
|
</span>
|
||||||
<span v-else>—</span>
|
<span v-else>—</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="pos.benefit && pos.benefit.length">
|
||||||
|
<span v-if="pos.benefit.includes('*')">{{ $t('falukant.politics.current.benefit_all') }}</span>
|
||||||
|
<span v-else>{{ pos.benefit.join(', ') }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="pos.termEnds">
|
<span v-if="pos.termEnds">
|
||||||
{{ formatDate(pos.termEnds) }}
|
{{ formatDate(pos.termEnds) }}
|
||||||
|
|||||||
Reference in New Issue
Block a user