fixes lang uand issues
This commit is contained in:
parent
1b51ee3e1c
commit
e43b0741ed
14 changed files with 275 additions and 166 deletions
|
|
@ -18,9 +18,13 @@ from modules.shared.configuration import APP_CONFIG
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Gemini-TTS speaker IDs from voices.list use short names (e.g. "Kore") and require model_name + prompt.
|
||||
# Gemini-TTS speaker IDs from voices.list use short names (e.g. "Kore") and require
|
||||
# SynthesisInput.prompt + VoiceSelectionParams.model_name (google-cloud-texttospeech >= 2.24.0).
|
||||
_GEMINI_TTS_DEFAULT_MODEL = "gemini-2.5-flash-tts"
|
||||
_GEMINI_TTS_NEUTRAL_PROMPT = "Say the following"
|
||||
_GEMINI_TTS_MIN_CLIENT_HINT = (
|
||||
"Gemini-TTS requires google-cloud-texttospeech>=2.24.0 (SynthesisInput.prompt, VoiceSelectionParams.model_name)."
|
||||
)
|
||||
|
||||
|
||||
class ConnectorGoogleSpeech:
|
||||
|
|
@ -940,7 +944,9 @@ class ConnectorGoogleSpeech:
|
|||
|
||||
logger.info(f"Using TTS voice: {selectedVoice} for language: {languageCode}")
|
||||
|
||||
if self._isGeminiTtsSpeakerVoiceName(selectedVoice):
|
||||
isGeminiVoice = self._isGeminiTtsSpeakerVoiceName(selectedVoice)
|
||||
|
||||
if isGeminiVoice:
|
||||
synthesisInput = texttospeech.SynthesisInput(
|
||||
text=text,
|
||||
prompt=_GEMINI_TTS_NEUTRAL_PROMPT,
|
||||
|
|
@ -958,19 +964,17 @@ class ConnectorGoogleSpeech:
|
|||
name=selectedVoice,
|
||||
ssml_gender=texttospeech.SsmlVoiceGender.NEUTRAL,
|
||||
)
|
||||
|
||||
# Select the type of audio file to return
|
||||
|
||||
audioConfig = texttospeech.AudioConfig(
|
||||
audio_encoding=texttospeech.AudioEncoding.MP3
|
||||
)
|
||||
|
||||
# Perform the text-to-speech request
|
||||
|
||||
response = self.tts_client.synthesize_speech(
|
||||
input=synthesisInput,
|
||||
voice=voice,
|
||||
audio_config=audioConfig
|
||||
)
|
||||
|
||||
|
||||
# Return the audio content
|
||||
return {
|
||||
"success": True,
|
||||
|
|
@ -982,9 +986,14 @@ class ConnectorGoogleSpeech:
|
|||
|
||||
except Exception as e:
|
||||
logger.error(f"Text-to-Speech error: {e}")
|
||||
detail = str(e)
|
||||
extra = ""
|
||||
low = detail.lower()
|
||||
if "prompt" in low or "model_name" in low or "unknown field" in low:
|
||||
extra = f" {_GEMINI_TTS_MIN_CLIENT_HINT}"
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Text-to-Speech failed: {str(e)}"
|
||||
"error": f"Text-to-Speech failed: {detail}{extra}",
|
||||
}
|
||||
|
||||
def _getDefaultVoice(self, languageCode: str) -> str:
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ class FileItem(PowerOnModel):
|
|||
description="Primary key",
|
||||
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||
)
|
||||
fileName: str = Field(
|
||||
description="Name of the file",
|
||||
json_schema_extra={"label": "Dateiname", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True},
|
||||
)
|
||||
mandateId: Optional[str] = Field(
|
||||
default="",
|
||||
description="ID of the mandate this file belongs to",
|
||||
|
|
@ -28,10 +32,6 @@ class FileItem(PowerOnModel):
|
|||
description="ID of the feature instance this file belongs to",
|
||||
json_schema_extra={"label": "Feature-Instanz", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "frontend_fk_source": "/api/features/instances", "frontend_fk_display_field": "label"},
|
||||
)
|
||||
fileName: str = Field(
|
||||
description="Name of the file",
|
||||
json_schema_extra={"label": "Dateiname", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True},
|
||||
)
|
||||
mimeType: str = Field(
|
||||
description="MIME type of the file",
|
||||
json_schema_extra={"label": "MIME-Typ", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from enum import Enum
|
|||
from datetime import datetime, timezone
|
||||
from pydantic import BaseModel, Field
|
||||
from modules.datamodels.datamodelBase import PowerOnModel
|
||||
from modules.shared.i18nRegistry import i18nModel
|
||||
from modules.shared.i18nRegistry import i18nModel, t
|
||||
import uuid
|
||||
|
||||
|
||||
|
|
@ -293,8 +293,8 @@ BUILTIN_PLANS: Dict[str, SubscriptionPlan] = {
|
|||
"ROOT": SubscriptionPlan(
|
||||
planKey="ROOT",
|
||||
selectableByUser=False,
|
||||
title="Root (System)",
|
||||
description="Interner Systemplan — keine Verrechnung.",
|
||||
title=t("Root (System)"),
|
||||
description=t("Interner Systemplan — keine Verrechnung."),
|
||||
billingPeriod=BillingPeriodEnum.NONE,
|
||||
autoRenew=False,
|
||||
maxUsers=None,
|
||||
|
|
@ -307,8 +307,8 @@ BUILTIN_PLANS: Dict[str, SubscriptionPlan] = {
|
|||
"TRIAL_14D": SubscriptionPlan(
|
||||
planKey="TRIAL_14D",
|
||||
selectableByUser=False,
|
||||
title="Gratis-Testphase (14 Tage)",
|
||||
description="14 Tage kostenlos testen — 1 User, 2 Module inklusive, CHF 25 AI-Budget.",
|
||||
title=t("Gratis-Testphase (14 Tage)"),
|
||||
description=t("14 Tage kostenlos testen — 1 User, 2 Module inklusive, CHF 25 AI-Budget."),
|
||||
billingPeriod=BillingPeriodEnum.NONE,
|
||||
autoRenew=False,
|
||||
maxUsers=1,
|
||||
|
|
@ -323,8 +323,8 @@ BUILTIN_PLANS: Dict[str, SubscriptionPlan] = {
|
|||
"STARTER_MONTHLY": SubscriptionPlan(
|
||||
planKey="STARTER_MONTHLY",
|
||||
selectableByUser=True,
|
||||
title="Starter (Monatlich)",
|
||||
description="CHF 69 pro User/Monat. 2 Module inklusive, CHF 25 AI-Budget pro User.",
|
||||
title=t("Starter (Monatlich)"),
|
||||
description=t("CHF 69 pro User/Monat. 2 Module inklusive, CHF 25 AI-Budget pro User."),
|
||||
billingPeriod=BillingPeriodEnum.MONTHLY,
|
||||
pricePerUserCHF=69.0,
|
||||
pricePerFeatureInstanceCHF=39.0,
|
||||
|
|
@ -337,8 +337,8 @@ BUILTIN_PLANS: Dict[str, SubscriptionPlan] = {
|
|||
"STARTER_YEARLY": SubscriptionPlan(
|
||||
planKey="STARTER_YEARLY",
|
||||
selectableByUser=True,
|
||||
title="Starter (Jaehrlich)",
|
||||
description="CHF 690 pro User/Jahr (-17%). 2 Module inklusive, CHF 25 AI-Budget pro User/Monat.",
|
||||
title=t("Starter (Jaehrlich)"),
|
||||
description=t("CHF 690 pro User/Jahr (-17%). 2 Module inklusive, CHF 25 AI-Budget pro User/Monat."),
|
||||
billingPeriod=BillingPeriodEnum.YEARLY,
|
||||
pricePerUserCHF=690.0,
|
||||
pricePerFeatureInstanceCHF=39.0,
|
||||
|
|
@ -351,8 +351,8 @@ BUILTIN_PLANS: Dict[str, SubscriptionPlan] = {
|
|||
"PROFESSIONAL_MONTHLY": SubscriptionPlan(
|
||||
planKey="PROFESSIONAL_MONTHLY",
|
||||
selectableByUser=True,
|
||||
title="Professional (Monatlich)",
|
||||
description="CHF 99 pro User/Monat. 5 Module inklusive, CHF 50 AI-Budget pro User.",
|
||||
title=t("Professional (Monatlich)"),
|
||||
description=t("CHF 99 pro User/Monat. 5 Module inklusive, CHF 50 AI-Budget pro User."),
|
||||
billingPeriod=BillingPeriodEnum.MONTHLY,
|
||||
pricePerUserCHF=99.0,
|
||||
pricePerFeatureInstanceCHF=29.0,
|
||||
|
|
@ -365,8 +365,8 @@ BUILTIN_PLANS: Dict[str, SubscriptionPlan] = {
|
|||
"PROFESSIONAL_YEARLY": SubscriptionPlan(
|
||||
planKey="PROFESSIONAL_YEARLY",
|
||||
selectableByUser=True,
|
||||
title="Professional (Jaehrlich)",
|
||||
description="CHF 990 pro User/Jahr (-17%). 5 Module inklusive, CHF 50 AI-Budget pro User/Monat.",
|
||||
title=t("Professional (Jaehrlich)"),
|
||||
description=t("CHF 990 pro User/Jahr (-17%). 5 Module inklusive, CHF 50 AI-Budget pro User/Monat."),
|
||||
billingPeriod=BillingPeriodEnum.YEARLY,
|
||||
pricePerUserCHF=990.0,
|
||||
pricePerFeatureInstanceCHF=29.0,
|
||||
|
|
@ -379,8 +379,8 @@ BUILTIN_PLANS: Dict[str, SubscriptionPlan] = {
|
|||
"MAX_MONTHLY": SubscriptionPlan(
|
||||
planKey="MAX_MONTHLY",
|
||||
selectableByUser=True,
|
||||
title="Max (Monatlich)",
|
||||
description="CHF 145 pro User/Monat. 15 Module inklusive, CHF 100 AI-Budget pro User.",
|
||||
title=t("Max (Monatlich)"),
|
||||
description=t("CHF 145 pro User/Monat. 15 Module inklusive, CHF 100 AI-Budget pro User."),
|
||||
billingPeriod=BillingPeriodEnum.MONTHLY,
|
||||
pricePerUserCHF=145.0,
|
||||
pricePerFeatureInstanceCHF=19.0,
|
||||
|
|
@ -393,8 +393,8 @@ BUILTIN_PLANS: Dict[str, SubscriptionPlan] = {
|
|||
"MAX_YEARLY": SubscriptionPlan(
|
||||
planKey="MAX_YEARLY",
|
||||
selectableByUser=True,
|
||||
title="Max (Jaehrlich)",
|
||||
description="CHF 1450 pro User/Jahr (-17%). 15 Module inklusive, CHF 100 AI-Budget pro User/Monat.",
|
||||
title=t("Max (Jaehrlich)"),
|
||||
description=t("CHF 1450 pro User/Jahr (-17%). 15 Module inklusive, CHF 100 AI-Budget pro User/Monat."),
|
||||
billingPeriod=BillingPeriodEnum.YEARLY,
|
||||
pricePerUserCHF=1450.0,
|
||||
pricePerFeatureInstanceCHF=19.0,
|
||||
|
|
|
|||
|
|
@ -1541,7 +1541,7 @@ class BillingObjects:
|
|||
return PaginatedResult(items=[], totalItems=0, totalPages=0)
|
||||
|
||||
recordFilter: Dict[str, Any] = {"accountId": accountIds}
|
||||
if scope == "personal" and userId:
|
||||
if userId:
|
||||
recordFilter["createdByUserId"] = userId
|
||||
|
||||
result = self.db.getRecordsetPaginated(
|
||||
|
|
@ -1622,7 +1622,7 @@ class BillingObjects:
|
|||
conditions = ['"accountId" = ANY(%s)', '"transactionType" = %s']
|
||||
values: list = [accountIds, "DEBIT"]
|
||||
|
||||
if scope == "personal" and userId:
|
||||
if userId:
|
||||
conditions.append('"createdByUserId" = %s')
|
||||
values.append(userId)
|
||||
|
||||
|
|
@ -1790,7 +1790,7 @@ class BillingObjects:
|
|||
return []
|
||||
|
||||
recordFilter: Dict[str, Any] = {"accountId": accountIds}
|
||||
if scope == "personal" and userId:
|
||||
if userId:
|
||||
recordFilter["createdByUserId"] = userId
|
||||
|
||||
if column in ("mandateName", "userName"):
|
||||
|
|
|
|||
|
|
@ -869,77 +869,66 @@ class ComponentObjects:
|
|||
sysCreatedAt=file.get("sysCreatedAt"),
|
||||
)
|
||||
|
||||
# Class-level cache — built once from the ExtractorRegistry
|
||||
_extensionToMime: Optional[Dict[str, str]] = None
|
||||
_textMimeTypes: Optional[set] = None
|
||||
|
||||
@classmethod
|
||||
def _ensureMimeMaps(cls):
|
||||
"""Lazily build extension→MIME and text-MIME-set from the ExtractorRegistry."""
|
||||
if cls._extensionToMime is not None:
|
||||
return
|
||||
try:
|
||||
from modules.serviceCenter.services.serviceExtraction.subRegistry import ExtractorRegistry
|
||||
registry = ExtractorRegistry()
|
||||
cls._extensionToMime = registry.getExtensionToMimeMap()
|
||||
|
||||
# Collect all MIME types declared by the TextExtractor (and other text-ish extractors)
|
||||
textMimes: set = set()
|
||||
seen: set = set()
|
||||
for ext in registry._map.values():
|
||||
eid = id(ext)
|
||||
if eid in seen:
|
||||
continue
|
||||
seen.add(eid)
|
||||
mimes = ext.getSupportedMimeTypes()
|
||||
if any(m.startswith("text/") for m in mimes):
|
||||
textMimes.update(mimes)
|
||||
# Always include common text types
|
||||
textMimes.update({
|
||||
"application/json", "application/xml", "application/javascript",
|
||||
"application/sql", "application/x-yaml", "application/x-toml",
|
||||
})
|
||||
cls._textMimeTypes = textMimes
|
||||
except Exception:
|
||||
cls._extensionToMime = {}
|
||||
cls._textMimeTypes = set()
|
||||
|
||||
def getMimeType(self, fileName: str) -> str:
|
||||
"""Determines the MIME type based on the file extension."""
|
||||
ext = os.path.splitext(fileName)[1].lower()[1:]
|
||||
extensionToMime = {
|
||||
"pdf": "application/pdf",
|
||||
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"doc": "application/msword",
|
||||
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"xls": "application/vnd.ms-excel",
|
||||
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"ppt": "application/vnd.ms-powerpoint",
|
||||
"csv": "text/csv",
|
||||
"txt": "text/plain",
|
||||
"json": "application/json",
|
||||
"xml": "application/xml",
|
||||
"html": "text/html",
|
||||
"htm": "text/html",
|
||||
"jpg": "image/jpeg",
|
||||
"jpeg": "image/jpeg",
|
||||
"png": "image/png",
|
||||
"gif": "image/gif",
|
||||
"webp": "image/webp",
|
||||
"svg": "image/svg+xml",
|
||||
"py": "text/x-python",
|
||||
"js": "application/javascript",
|
||||
"css": "text/css",
|
||||
"eml": "message/rfc822",
|
||||
"msg": "application/vnd.ms-outlook",
|
||||
}
|
||||
return extensionToMime.get(ext.lower(), "application/octet-stream")
|
||||
"""Determines the MIME type based on the file extension.
|
||||
|
||||
Resolution order:
|
||||
1. ExtractorRegistry (derived from all registered extractors)
|
||||
2. Python stdlib mimetypes.guess_type
|
||||
3. Fallback: application/octet-stream
|
||||
"""
|
||||
self._ensureMimeMaps()
|
||||
ext = os.path.splitext(fileName)[1].lower().lstrip('.')
|
||||
if ext and ext in self._extensionToMime:
|
||||
return self._extensionToMime[ext]
|
||||
guessed, _ = mimetypes.guess_type(fileName, strict=False)
|
||||
return guessed or "application/octet-stream"
|
||||
|
||||
def isTextMimeType(self, mimeType: str) -> bool:
|
||||
"""Determines if a MIME type represents a text-based format."""
|
||||
textMimeTypes = {
|
||||
'text/plain',
|
||||
'text/html',
|
||||
'text/css',
|
||||
'text/javascript',
|
||||
'text/x-python',
|
||||
'text/csv',
|
||||
'text/xml',
|
||||
'application/json',
|
||||
'application/xml',
|
||||
'application/javascript',
|
||||
'application/x-python',
|
||||
'application/x-httpd-php',
|
||||
'application/x-sh',
|
||||
'application/x-shellscript',
|
||||
'application/x-yaml',
|
||||
'application/x-toml',
|
||||
'application/x-markdown',
|
||||
'application/x-latex',
|
||||
'application/x-tex',
|
||||
'application/x-rst',
|
||||
'application/x-asciidoc',
|
||||
'application/x-markdown',
|
||||
'application/x-httpd-php',
|
||||
'application/x-httpd-php-source',
|
||||
'application/x-httpd-php3',
|
||||
'application/x-httpd-php4',
|
||||
'application/x-httpd-php5',
|
||||
'application/x-httpd-php7',
|
||||
'application/x-httpd-php8',
|
||||
'application/x-httpd-php-source',
|
||||
'application/x-httpd-php3-source',
|
||||
'application/x-httpd-php4-source',
|
||||
'application/x-httpd-php5-source',
|
||||
'application/x-httpd-php7-source',
|
||||
'application/x-httpd-php8-source'
|
||||
}
|
||||
return mimeType.lower() in textMimeTypes
|
||||
"""Determines if a MIME type represents a text-based format.
|
||||
|
||||
Derived from the MIME types declared by text-oriented extractors.
|
||||
"""
|
||||
self._ensureMimeMaps()
|
||||
lower = mimeType.lower()
|
||||
if lower.startswith("text/"):
|
||||
return True
|
||||
return lower in self._textMimeTypes
|
||||
|
||||
# File methods - metadata-based operations
|
||||
|
||||
|
|
|
|||
|
|
@ -869,7 +869,12 @@ def _buildTemplateRolesList(featureCode: Optional[str] = None) -> List[Dict[str,
|
|||
rootInterface = getRootInterface()
|
||||
featureInterface = getFeatureInterface(rootInterface.db)
|
||||
roles = featureInterface.getTemplateRoles(featureCode)
|
||||
return [r.model_dump() for r in roles]
|
||||
result = []
|
||||
for r in roles:
|
||||
d = r.model_dump()
|
||||
d["description"] = resolveText(r.description)
|
||||
result.append(d)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/templates/roles")
|
||||
|
|
@ -1546,7 +1551,7 @@ def get_feature_instance_available_roles(
|
|||
result.append({
|
||||
"id": role.id,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": role.description or {},
|
||||
"description": resolveText(role.description),
|
||||
"featureCode": role.featureCode,
|
||||
"isSystemRole": role.isSystemRole
|
||||
})
|
||||
|
|
|
|||
|
|
@ -911,13 +911,13 @@ def list_roles(
|
|||
result.append({
|
||||
"id": role.id,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": resolveText(role.description),
|
||||
"description": role.description.model_dump() if hasattr(role.description, 'model_dump') else role.description,
|
||||
"mandateId": role.mandateId,
|
||||
"featureInstanceId": role.featureInstanceId,
|
||||
"featureCode": role.featureCode,
|
||||
"userCount": roleCounts.get(str(role.id), 0),
|
||||
"isSystemRole": role.isSystemRole,
|
||||
"scopeType": scopeType # Computed field for frontend display
|
||||
"scopeType": scopeType
|
||||
})
|
||||
|
||||
# MandateAdmin: filter to only roles in admin's mandates
|
||||
|
|
@ -1040,7 +1040,7 @@ def get_roles_filter_values(
|
|||
result.append({
|
||||
"id": role.id,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": resolveText(role.description),
|
||||
"description": role.description.model_dump() if hasattr(role.description, 'model_dump') else role.description,
|
||||
"mandateId": role.mandateId,
|
||||
"featureInstanceId": role.featureInstanceId,
|
||||
"featureCode": role.featureCode,
|
||||
|
|
@ -1095,7 +1095,7 @@ def create_role(
|
|||
return {
|
||||
"id": createdRole.id,
|
||||
"roleLabel": createdRole.roleLabel,
|
||||
"description": createdRole.description,
|
||||
"description": createdRole.description.model_dump() if hasattr(createdRole.description, 'model_dump') else createdRole.description,
|
||||
"mandateId": createdRole.mandateId,
|
||||
"featureInstanceId": createdRole.featureInstanceId,
|
||||
"featureCode": createdRole.featureCode,
|
||||
|
|
@ -1157,7 +1157,7 @@ def get_role(
|
|||
return {
|
||||
"id": role.id,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": resolveText(role.description),
|
||||
"description": role.description.model_dump() if hasattr(role.description, 'model_dump') else role.description,
|
||||
"mandateId": role.mandateId,
|
||||
"featureInstanceId": role.featureInstanceId,
|
||||
"featureCode": role.featureCode,
|
||||
|
|
@ -1220,7 +1220,7 @@ def update_role(
|
|||
return {
|
||||
"id": updatedRole.id,
|
||||
"roleLabel": updatedRole.roleLabel,
|
||||
"description": updatedRole.description,
|
||||
"description": updatedRole.description.model_dump() if hasattr(updatedRole.description, 'model_dump') else updatedRole.description,
|
||||
"mandateId": updatedRole.mandateId,
|
||||
"featureInstanceId": updatedRole.featureInstanceId,
|
||||
"featureCode": updatedRole.featureCode,
|
||||
|
|
|
|||
|
|
@ -1633,6 +1633,7 @@ def getUserViewStatistics(
|
|||
month: Optional[int] = Query(None, description="Month (1-12, required for period='day')"),
|
||||
scope: str = Query(default="all", description="Scope: 'personal' (own costs only), 'mandate' (filter by mandateId), 'all' (RBAC-filtered)"),
|
||||
mandateId: Optional[str] = Query(None, description="Mandate ID filter (used with scope='mandate')"),
|
||||
onlyMine: Optional[bool] = Query(None, description="Additional filter: restrict to current user's transactions within the selected scope"),
|
||||
ctx: RequestContext = Depends(getRequestContext)
|
||||
) -> ViewStatisticsResponse:
|
||||
"""
|
||||
|
|
@ -1643,6 +1644,9 @@ def getUserViewStatistics(
|
|||
- mandate: transactions for a specific mandate (requires mandateId parameter)
|
||||
- all: RBAC-filtered (SysAdmin sees everything, admin sees mandate, user sees own)
|
||||
|
||||
onlyMine: additional filter that restricts results to the current user's
|
||||
transactions while keeping the scope-based mandate selection.
|
||||
|
||||
- period='month': returns monthly time series for the given year
|
||||
- period='day': returns daily time series for the given month/year
|
||||
"""
|
||||
|
|
@ -1668,7 +1672,7 @@ def getUserViewStatistics(
|
|||
if scope == "mandate" and mandateId:
|
||||
loadMandateIds = [mandateId]
|
||||
|
||||
personalUserId = str(ctx.user.id) if scope == "personal" else None
|
||||
personalUserId = str(ctx.user.id) if (scope == "personal" or onlyMine) else None
|
||||
|
||||
if period == "day":
|
||||
startDate = date(year, month, 1)
|
||||
|
|
@ -1745,6 +1749,7 @@ def getUserViewTransactions(
|
|||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
|
||||
scope: str = Query(default="all", description="Scope: 'personal' (own costs only), 'mandate' (filter by mandateId), 'all' (RBAC-filtered)"),
|
||||
mandateId: Optional[str] = Query(None, description="Mandate ID filter (used with scope='mandate')"),
|
||||
onlyMine: Optional[bool] = Query(None, description="Additional filter: restrict to current user's transactions within the selected scope"),
|
||||
ctx: RequestContext = Depends(getRequestContext)
|
||||
) -> PaginatedResponse[UserTransactionResponse]:
|
||||
"""
|
||||
|
|
@ -1755,10 +1760,14 @@ def getUserViewTransactions(
|
|||
- mandate: transactions for a specific mandate (requires mandateId parameter)
|
||||
- all: RBAC-filtered (SysAdmin sees everything, admin sees mandate, user sees own)
|
||||
|
||||
onlyMine: additional filter that restricts results to the current user's
|
||||
transactions while keeping the scope-based mandate selection.
|
||||
|
||||
Query Parameters:
|
||||
- pagination: JSON-encoded PaginationParams object, or None for no pagination
|
||||
- scope: 'personal', 'mandate', or 'all'
|
||||
- mandateId: required when scope='mandate'
|
||||
- onlyMine: true to restrict to current user's data within the scope
|
||||
"""
|
||||
try:
|
||||
billingInterface = getBillingInterface(ctx.user, ctx.mandateId)
|
||||
|
|
@ -1783,7 +1792,7 @@ def getUserViewTransactions(
|
|||
loadMandateIds = [mandateId]
|
||||
|
||||
effectiveScope = scope
|
||||
personalUserId = str(ctx.user.id) if scope == "personal" else None
|
||||
personalUserId = str(ctx.user.id) if (scope == "personal" or onlyMine) else None
|
||||
|
||||
if not paginationParams:
|
||||
paginationParams = PaginationParams(page=1, pageSize=50)
|
||||
|
|
@ -1844,6 +1853,7 @@ def getUserViewTransactionsFilterValues(
|
|||
pagination: Optional[str] = Query(None, description="JSON-encoded current filters"),
|
||||
scope: str = Query(default="all", description="Scope: 'personal', 'mandate', 'all'"),
|
||||
mandateId: Optional[str] = Query(None, description="Mandate ID filter (used with scope='mandate')"),
|
||||
onlyMine: Optional[bool] = Query(None, description="Additional filter: restrict to current user's data within the selected scope"),
|
||||
ctx: RequestContext = Depends(getRequestContext)
|
||||
):
|
||||
"""Return distinct filter values for a column in user transactions (SQL DISTINCT)."""
|
||||
|
|
@ -1876,7 +1886,7 @@ def getUserViewTransactionsFilterValues(
|
|||
except (json.JSONDecodeError, ValueError):
|
||||
pass
|
||||
|
||||
personalUserId = str(ctx.user.id) if scope == "personal" else None
|
||||
personalUserId = str(ctx.user.id) if (scope == "personal" or onlyMine) else None
|
||||
|
||||
return billingInterface.getTransactionDistinctValues(
|
||||
mandateIds=loadMandateIds,
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ router = APIRouter(
|
|||
)
|
||||
|
||||
@router.get("/list", response_model=PaginatedResponse[FileItem])
|
||||
@limiter.limit("30/minute")
|
||||
@limiter.limit("120/minute")
|
||||
def get_files(
|
||||
request: Request,
|
||||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
|
||||
|
|
|
|||
|
|
@ -1063,10 +1063,15 @@ async def import_language_sets(
|
|||
_TRANSLATE_FIELD_MAX_LEN = 2000
|
||||
|
||||
|
||||
class _TargetLang(BaseModel):
|
||||
code: str = Field(..., min_length=2, max_length=10)
|
||||
label: str = Field(default="")
|
||||
|
||||
|
||||
class TranslateFieldRequest(BaseModel):
|
||||
sourceText: str = Field(..., min_length=1, max_length=_TRANSLATE_FIELD_MAX_LEN)
|
||||
sourceLang: str = Field(default="de", min_length=2, max_length=5)
|
||||
targetLangs: List[str] = Field(..., min_length=1)
|
||||
targetLangs: List[_TargetLang] = Field(..., min_length=1)
|
||||
|
||||
|
||||
@router.post("/translate-field")
|
||||
|
|
@ -1076,7 +1081,7 @@ async def translateField(
|
|||
currentUser: User = Depends(getCurrentUser),
|
||||
):
|
||||
"""Translate a single text into one or more target languages (for TextMultilingual fields)."""
|
||||
targets = [c for c in body.targetLangs if c != body.sourceLang]
|
||||
targets = [t for t in body.targetLangs if t.code != body.sourceLang]
|
||||
if not targets:
|
||||
return {"translations": {}}
|
||||
|
||||
|
|
@ -1084,12 +1089,12 @@ async def translateField(
|
|||
billingCb = _makeBillingCallback(currentUser, mandateId)
|
||||
|
||||
results: Dict[str, str] = {}
|
||||
for targetCode in targets:
|
||||
targetLabel = _ISO_LABELS.get(targetCode, targetCode)
|
||||
for target in targets:
|
||||
targetLabel = target.label or _ISO_LABELS.get(target.code, target.code)
|
||||
keysToTranslate = {body.sourceText: "TextMultilingual field"}
|
||||
translated = await _translateBatch(keysToTranslate, targetLabel, targetCode, billingCb)
|
||||
translated = await _translateBatch(keysToTranslate, targetLabel, target.code, billingCb)
|
||||
val = translated.get(body.sourceText, "")
|
||||
if val:
|
||||
results[targetCode] = val
|
||||
results[target.code] = val
|
||||
|
||||
return {"translations": results}
|
||||
|
|
|
|||
|
|
@ -23,12 +23,22 @@ from pydantic import BaseModel, Field
|
|||
from modules.auth import limiter, getRequestContext, RequestContext
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||
from modules.routes.routeDataUsers import _applyFiltersAndSort, _extractDistinctValues
|
||||
from modules.shared.i18nRegistry import apiRouteContext
|
||||
from modules.shared.i18nRegistry import apiRouteContext, resolveText
|
||||
routeApiMsg = apiRouteContext("routeSubscription")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _planToDict(plan) -> Optional[Dict[str, Any]]:
|
||||
"""Serialize a SubscriptionPlan with resolved i18n title/description."""
|
||||
if not plan:
|
||||
return None
|
||||
d = plan.model_dump()
|
||||
d["title"] = resolveText(plan.title)
|
||||
d["description"] = resolveText(plan.description)
|
||||
return d
|
||||
|
||||
|
||||
def _resolveMandateId(context: RequestContext) -> str:
|
||||
if context.mandateId:
|
||||
return str(context.mandateId)
|
||||
|
|
@ -78,11 +88,56 @@ class ForceCancelRequest(BaseModel):
|
|||
class VerifyCheckoutRequest(BaseModel):
|
||||
sessionId: str = Field(..., description="Stripe Checkout Session ID to verify")
|
||||
|
||||
class SubscriptionUsage(BaseModel):
|
||||
activeUsers: int = 0
|
||||
activeInstances: int = 0
|
||||
usedStorageMB: float = 0
|
||||
maxStorageMB: Optional[float] = None
|
||||
storagePercent: Optional[float] = None
|
||||
|
||||
class SubscriptionStatusResponse(BaseModel):
|
||||
active: bool
|
||||
subscription: Optional[Dict[str, Any]] = None
|
||||
plan: Optional[Dict[str, Any]] = None
|
||||
scheduled: Optional[Dict[str, Any]] = None
|
||||
usage: Optional[SubscriptionUsage] = None
|
||||
|
||||
|
||||
def _computeUsage(mandateId: str, plan) -> SubscriptionUsage:
|
||||
"""Compute current usage metrics for a mandate's subscription."""
|
||||
try:
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelMembership import UserMandate
|
||||
from modules.datamodels.datamodelFeatures import FeatureInstance
|
||||
from modules.interfaces.interfaceDbKnowledge import aggregateMandateRagTotalBytes
|
||||
|
||||
rootIf = getRootInterface()
|
||||
|
||||
allUM = rootIf.db.getRecordset(UserMandate, recordFilter={"mandateId": mandateId})
|
||||
activeUsers = len(allUM) if allUM else 0
|
||||
|
||||
allFI = rootIf.db.getRecordset(FeatureInstance, recordFilter={"mandateId": mandateId})
|
||||
activeInstances = sum(
|
||||
1 for fi in (allFI or [])
|
||||
if (fi.get("enabled") if isinstance(fi, dict) else getattr(fi, "enabled", False))
|
||||
)
|
||||
|
||||
ragBytes = aggregateMandateRagTotalBytes(mandateId)
|
||||
usedMB = round(ragBytes / (1024 * 1024), 2)
|
||||
|
||||
maxMB = plan.maxDataVolumeMB if plan else None
|
||||
storagePercent = round((usedMB / maxMB) * 100, 1) if maxMB else None
|
||||
|
||||
return SubscriptionUsage(
|
||||
activeUsers=activeUsers,
|
||||
activeInstances=activeInstances,
|
||||
usedStorageMB=usedMB,
|
||||
maxStorageMB=maxMB,
|
||||
storagePercent=storagePercent,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to compute subscription usage: %s", e)
|
||||
return SubscriptionUsage()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
|
@ -110,7 +165,7 @@ def getPlans(request: Request, context: RequestContext = Depends(getRequestConte
|
|||
mandateId = _resolveMandateId(context)
|
||||
subService = getSubscriptionService(context.user, mandateId)
|
||||
plans = subService.getSelectablePlans()
|
||||
return [p.model_dump() for p in plans]
|
||||
return [_planToDict(p) for p in plans]
|
||||
except Exception as e:
|
||||
logger.error("Error fetching plans: %s", e)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
@ -142,17 +197,21 @@ def getStatus(request: Request, context: RequestContext = Depends(getRequestCont
|
|||
return SubscriptionStatusResponse(
|
||||
active=False,
|
||||
subscription=sub,
|
||||
plan=plan.model_dump() if plan else None,
|
||||
plan=_planToDict(plan),
|
||||
scheduled=scheduled,
|
||||
)
|
||||
return SubscriptionStatusResponse(active=False, scheduled=scheduled)
|
||||
|
||||
plan = subService.getPlan(operative.get("planKey", ""))
|
||||
|
||||
usage = _computeUsage(mandateId, plan)
|
||||
|
||||
return SubscriptionStatusResponse(
|
||||
active=True,
|
||||
subscription=operative,
|
||||
plan=plan.model_dump() if plan else None,
|
||||
plan=_planToDict(plan),
|
||||
scheduled=scheduled,
|
||||
usage=usage,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error fetching status: %s", e)
|
||||
|
|
@ -394,7 +453,7 @@ def _buildEnrichedSubscriptions() -> List[Dict[str, Any]]:
|
|||
plan = BUILTIN_PLANS.get(planKey)
|
||||
|
||||
sub["mandateName"] = mandateNames.get(mid, mid[:8])
|
||||
sub["planTitle"] = (plan.title or planKey) if plan else planKey
|
||||
sub["planTitle"] = resolveText(plan.title) if plan else planKey
|
||||
|
||||
if sub.get("status") in operativeValues:
|
||||
userPrice = sub.get("snapshotPricePerUserCHF", 0) or 0
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from slowapi.util import get_remote_address
|
|||
|
||||
from modules.auth.authentication import getRequestContext, RequestContext
|
||||
from modules.system.mainSystem import NAVIGATION_SECTIONS, _objectKeyToUiComponent
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.interfaces.interfaceFeatures import getFeatureInterface
|
||||
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
|
||||
|
|
@ -386,8 +387,8 @@ def _buildStaticBlocks(
|
|||
"""
|
||||
Build static navigation blocks from NAVIGATION_SECTIONS.
|
||||
|
||||
Labels/titles are plain German strings (i18n base keys).
|
||||
The frontend translates them via t().
|
||||
Keys are registered at import time via t() in mainSystem.py.
|
||||
At request time, resolveText() translates them to the current language.
|
||||
"""
|
||||
blocks = []
|
||||
|
||||
|
|
@ -407,7 +408,7 @@ def _buildStaticBlocks(
|
|||
if subItems:
|
||||
filteredSubgroups.append({
|
||||
"id": subgroup["id"],
|
||||
"title": subgroup["title"],
|
||||
"title": resolveText(subgroup["title"]),
|
||||
"order": subgroup.get("order", 50),
|
||||
"items": subItems,
|
||||
})
|
||||
|
|
@ -424,7 +425,7 @@ def _buildStaticBlocks(
|
|||
blocks.append({
|
||||
"type": "static",
|
||||
"id": section["id"],
|
||||
"title": section["title"],
|
||||
"title": resolveText(section["title"]),
|
||||
"order": section.get("order", 50),
|
||||
"items": topLevelItems,
|
||||
"subgroups": filteredSubgroups,
|
||||
|
|
@ -438,7 +439,7 @@ def _buildStaticBlocks(
|
|||
blocks.append({
|
||||
"type": "static",
|
||||
"id": section["id"],
|
||||
"title": section["title"],
|
||||
"title": resolveText(section["title"]),
|
||||
"order": section.get("order", 50),
|
||||
"items": filteredItems,
|
||||
})
|
||||
|
|
@ -447,18 +448,13 @@ def _buildStaticBlocks(
|
|||
|
||||
|
||||
def _formatBlockItem(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Format a navigation item for the API response.
|
||||
|
||||
Labels are plain German strings (i18n base keys).
|
||||
The frontend translates them via t().
|
||||
"""
|
||||
"""Format a navigation item for the API response."""
|
||||
objectKey = item["objectKey"]
|
||||
uiComponent = _objectKeyToUiComponent(objectKey)
|
||||
|
||||
return {
|
||||
"uiComponent": uiComponent,
|
||||
"uiLabel": item["label"],
|
||||
"uiLabel": resolveText(item["label"]),
|
||||
"uiPath": item["path"],
|
||||
"order": item.get("order", 50),
|
||||
"objectKey": objectKey,
|
||||
|
|
|
|||
|
|
@ -139,6 +139,40 @@ class ExtractorRegistry:
|
|||
return self._map[ext]
|
||||
return self._fallback
|
||||
|
||||
def getExtensionToMimeMap(self) -> Dict[str, str]:
|
||||
"""Build a map from file extension (without dot) to primary MIME type.
|
||||
|
||||
Iterates all registered extractors and pairs each declared extension
|
||||
with the first MIME type declared by the same extractor. Specialized
|
||||
extractors (Pdf, Docx, …) are processed first so their mapping wins
|
||||
over broad extractors like TextExtractor.
|
||||
"""
|
||||
extMap: Dict[str, str] = {}
|
||||
seen: set = set()
|
||||
|
||||
# Collect unique extractor instances (same instance registered under many keys)
|
||||
extractors: list[Extractor] = []
|
||||
for ext in self._map.values():
|
||||
eid = id(ext)
|
||||
if eid not in seen:
|
||||
seen.add(eid)
|
||||
extractors.append(ext)
|
||||
|
||||
# Specialized (few extensions) first so they win over broad ones
|
||||
extractors.sort(key=lambda e: len(e.getSupportedExtensions()))
|
||||
|
||||
for ext in extractors:
|
||||
extensions = ext.getSupportedExtensions()
|
||||
mimeTypes = ext.getSupportedMimeTypes()
|
||||
if not extensions or not mimeTypes:
|
||||
continue
|
||||
primaryMime = mimeTypes[0]
|
||||
for rawExt in extensions:
|
||||
key = rawExt.lstrip('.').lower()
|
||||
if key not in extMap:
|
||||
extMap[key] = primaryMime
|
||||
return extMap
|
||||
|
||||
def getAllSupportedFormats(self) -> Dict[str, Dict[str, list[str]]]:
|
||||
"""
|
||||
Get all supported formats from all registered extractors.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ Also defines the navigation structure for the frontend.
|
|||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# System metadata
|
||||
|
|
@ -38,13 +40,13 @@ NAVIGATION_SECTIONS = [
|
|||
# ─── Meine Sicht (with top-level item + subgroups) ───
|
||||
{
|
||||
"id": "system",
|
||||
"title": "Meine Sicht",
|
||||
"title": t("Meine Sicht"),
|
||||
"order": 10,
|
||||
"items": [
|
||||
{
|
||||
"id": "home",
|
||||
"objectKey": "ui.system.home",
|
||||
"label": "Übersicht",
|
||||
"label": t("Übersicht"),
|
||||
"icon": "FaHome",
|
||||
"path": "/",
|
||||
"order": 10,
|
||||
|
|
@ -55,13 +57,13 @@ NAVIGATION_SECTIONS = [
|
|||
# ── Basisdaten ──
|
||||
{
|
||||
"id": "system-basedata",
|
||||
"title": "Basisdaten",
|
||||
"title": t("Basisdaten"),
|
||||
"order": 20,
|
||||
"items": [
|
||||
{
|
||||
"id": "connections",
|
||||
"objectKey": "ui.system.connections",
|
||||
"label": "Verbindungen",
|
||||
"label": t("Verbindungen"),
|
||||
"icon": "FaLink",
|
||||
"path": "/basedata/connections",
|
||||
"order": 10,
|
||||
|
|
@ -69,7 +71,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "files",
|
||||
"objectKey": "ui.system.files",
|
||||
"label": "Dateien",
|
||||
"label": t("Dateien"),
|
||||
"icon": "FaRegFileAlt",
|
||||
"path": "/basedata/files",
|
||||
"order": 20,
|
||||
|
|
@ -77,7 +79,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "prompts",
|
||||
"objectKey": "ui.system.prompts",
|
||||
"label": "Prompts",
|
||||
"label": t("Prompts"),
|
||||
"icon": "FaLightbulb",
|
||||
"path": "/basedata/prompts",
|
||||
"order": 30,
|
||||
|
|
@ -87,13 +89,13 @@ NAVIGATION_SECTIONS = [
|
|||
# ── Nutzung ──
|
||||
{
|
||||
"id": "system-usage",
|
||||
"title": "Nutzung",
|
||||
"title": t("Nutzung"),
|
||||
"order": 30,
|
||||
"items": [
|
||||
{
|
||||
"id": "billing-admin",
|
||||
"objectKey": "ui.system.billingAdmin",
|
||||
"label": "Abrechnung",
|
||||
"label": t("Abrechnung"),
|
||||
"icon": "FaMoneyBillAlt",
|
||||
"path": "/billing/admin",
|
||||
"order": 10,
|
||||
|
|
@ -101,7 +103,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "statistics",
|
||||
"objectKey": "ui.system.statistics",
|
||||
"label": "Statistiken",
|
||||
"label": t("Statistiken"),
|
||||
"icon": "FaChartBar",
|
||||
"path": "/billing/transactions",
|
||||
"order": 20,
|
||||
|
|
@ -109,7 +111,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "automations",
|
||||
"objectKey": "ui.system.automations",
|
||||
"label": "Automations",
|
||||
"label": t("Automations"),
|
||||
"icon": "FaRobot",
|
||||
"path": "/automations",
|
||||
"order": 30,
|
||||
|
|
@ -117,7 +119,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "store",
|
||||
"objectKey": "ui.system.store",
|
||||
"label": "Store",
|
||||
"label": t("Store"),
|
||||
"icon": "FaStore",
|
||||
"path": "/store",
|
||||
"order": 40,
|
||||
|
|
@ -126,7 +128,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "settings",
|
||||
"objectKey": "ui.system.settings",
|
||||
"label": "Einstellungen",
|
||||
"label": t("Einstellungen"),
|
||||
"icon": "FaCog",
|
||||
"path": "/settings",
|
||||
"order": 50,
|
||||
|
|
@ -139,19 +141,19 @@ NAVIGATION_SECTIONS = [
|
|||
# ─── Administration (with subgroups) ───
|
||||
{
|
||||
"id": "admin",
|
||||
"title": "Administration",
|
||||
"title": t("Administration"),
|
||||
"order": 200,
|
||||
"subgroups": [
|
||||
# ── Wizards ──
|
||||
{
|
||||
"id": "admin-wizards",
|
||||
"title": "Wizards",
|
||||
"title": t("Wizards"),
|
||||
"order": 10,
|
||||
"items": [
|
||||
{
|
||||
"id": "admin-mandate-wizard",
|
||||
"objectKey": "ui.admin.mandateWizard",
|
||||
"label": "Mandanten-Wizard",
|
||||
"label": t("Mandanten-Wizard"),
|
||||
"icon": "FaMagic",
|
||||
"path": "/admin/mandate-wizard",
|
||||
"order": 10,
|
||||
|
|
@ -160,7 +162,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "admin-invitation-wizard",
|
||||
"objectKey": "ui.admin.invitationWizard",
|
||||
"label": "Einladungs-Wizard",
|
||||
"label": t("Einladungs-Wizard"),
|
||||
"icon": "FaEnvelopeOpenText",
|
||||
"path": "/admin/invitation-wizard",
|
||||
"order": 20,
|
||||
|
|
@ -171,13 +173,13 @@ NAVIGATION_SECTIONS = [
|
|||
# ── Users ──
|
||||
{
|
||||
"id": "admin-users-group",
|
||||
"title": "Benutzer",
|
||||
"title": t("Benutzer"),
|
||||
"order": 20,
|
||||
"items": [
|
||||
{
|
||||
"id": "admin-users",
|
||||
"objectKey": "ui.admin.users",
|
||||
"label": "Benutzer",
|
||||
"label": t("Benutzer"),
|
||||
"icon": "FaUsers",
|
||||
"path": "/admin/users",
|
||||
"order": 10,
|
||||
|
|
@ -186,7 +188,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "admin-invitations",
|
||||
"objectKey": "ui.admin.invitations",
|
||||
"label": "Benutzer-Einladungen",
|
||||
"label": t("Benutzer-Einladungen"),
|
||||
"icon": "FaEnvelopeOpenText",
|
||||
"path": "/admin/invitations",
|
||||
"order": 20,
|
||||
|
|
@ -195,7 +197,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "admin-user-access-overview",
|
||||
"objectKey": "ui.admin.userAccessOverview",
|
||||
"label": "Benutzer-Zugriffsübersicht",
|
||||
"label": t("Benutzer-Zugriffsübersicht"),
|
||||
"icon": "FaClipboardList",
|
||||
"path": "/admin/user-access-overview",
|
||||
"order": 30,
|
||||
|
|
@ -204,7 +206,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "admin-subscriptions",
|
||||
"objectKey": "ui.admin.subscriptions",
|
||||
"label": "Abonnements",
|
||||
"label": t("Abonnements"),
|
||||
"icon": "FaFileContract",
|
||||
"path": "/admin/subscriptions",
|
||||
"order": 40,
|
||||
|
|
@ -215,13 +217,13 @@ NAVIGATION_SECTIONS = [
|
|||
# ── System ──
|
||||
{
|
||||
"id": "admin-system-group",
|
||||
"title": "System",
|
||||
"title": t("System"),
|
||||
"order": 30,
|
||||
"items": [
|
||||
{
|
||||
"id": "admin-roles",
|
||||
"objectKey": "ui.admin.roles",
|
||||
"label": "Rollen",
|
||||
"label": t("Rollen"),
|
||||
"icon": "FaUserTag",
|
||||
"path": "/admin/mandate-roles",
|
||||
"order": 10,
|
||||
|
|
@ -230,7 +232,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "admin-mandate-role-permissions",
|
||||
"objectKey": "ui.admin.mandateRolePermissions",
|
||||
"label": "Rollen-Berechtigungen",
|
||||
"label": t("Rollen-Berechtigungen"),
|
||||
"icon": "FaKey",
|
||||
"path": "/admin/mandate-role-permissions",
|
||||
"order": 20,
|
||||
|
|
@ -239,7 +241,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "admin-mandates",
|
||||
"objectKey": "ui.admin.mandates",
|
||||
"label": "Mandanten",
|
||||
"label": t("Mandanten"),
|
||||
"icon": "FaBuilding",
|
||||
"path": "/admin/mandates",
|
||||
"order": 30,
|
||||
|
|
@ -248,7 +250,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "admin-user-mandates",
|
||||
"objectKey": "ui.admin.userMandates",
|
||||
"label": "Mandanten-Mitglieder",
|
||||
"label": t("Mandanten-Mitglieder"),
|
||||
"icon": "FaUserFriends",
|
||||
"path": "/admin/user-mandates",
|
||||
"order": 40,
|
||||
|
|
@ -257,7 +259,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "admin-access",
|
||||
"objectKey": "ui.admin.access",
|
||||
"label": "Zugriffsverwaltung",
|
||||
"label": t("Zugriffsverwaltung"),
|
||||
"icon": "FaBuilding",
|
||||
"path": "/admin/access",
|
||||
"order": 50,
|
||||
|
|
@ -266,7 +268,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "admin-feature-instances",
|
||||
"objectKey": "ui.admin.featureInstances",
|
||||
"label": "Feature-Instanzen",
|
||||
"label": t("Feature-Instanzen"),
|
||||
"icon": "FaCubes",
|
||||
"path": "/admin/feature-instances",
|
||||
"order": 60,
|
||||
|
|
@ -275,7 +277,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "admin-feature-roles",
|
||||
"objectKey": "ui.admin.featureRoles",
|
||||
"label": "Features Rollen-Vorlagen",
|
||||
"label": t("Features Rollen-Vorlagen"),
|
||||
"icon": "FaShieldAlt",
|
||||
"path": "/admin/feature-roles",
|
||||
"order": 70,
|
||||
|
|
@ -285,7 +287,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "admin-logs",
|
||||
"objectKey": "ui.admin.logs",
|
||||
"label": "Logs",
|
||||
"label": t("Logs"),
|
||||
"icon": "FaFileAlt",
|
||||
"path": "/admin/logs",
|
||||
"order": 90,
|
||||
|
|
@ -295,7 +297,7 @@ NAVIGATION_SECTIONS = [
|
|||
{
|
||||
"id": "admin-languages",
|
||||
"objectKey": "ui.admin.languages",
|
||||
"label": "UI-Sprachen",
|
||||
"label": t("UI-Sprachen"),
|
||||
"icon": "FaGlobe",
|
||||
"path": "/admin/languages",
|
||||
"order": 95,
|
||||
|
|
|
|||
Loading…
Reference in a new issue