improved UX in dashboard
This commit is contained in:
272
common.php
272
common.php
@ -247,12 +247,12 @@ function qdb_upsert_source_translation(PDO $pdo, string $type, string $id, strin
|
||||
}
|
||||
|
||||
/** Catalog of global app UI string keys (LanguageManager). */
|
||||
function qdb_app_string_catalog(): array {
|
||||
function qdb_app_string_catalog(bool $reload = false): array {
|
||||
static $catalog = null;
|
||||
if ($catalog !== null) {
|
||||
if (!$reload && $catalog !== null) {
|
||||
return $catalog;
|
||||
}
|
||||
$path = __DIR__ . '/data/app_ui_strings.json';
|
||||
$path = qdb_app_string_catalog_path();
|
||||
if (!is_readable($path)) {
|
||||
$catalog = ['keys' => [], 'germanDefaults' => []];
|
||||
return $catalog;
|
||||
@ -262,6 +262,49 @@ function qdb_app_string_catalog(): array {
|
||||
return $catalog;
|
||||
}
|
||||
|
||||
function qdb_app_string_catalog_path(): string {
|
||||
return __DIR__ . '/data/app_ui_strings.json';
|
||||
}
|
||||
|
||||
/** Remove questionnaire_group_* from catalog file (keys list + germanDefaults). */
|
||||
function qdb_remove_questionnaire_group_from_catalog(string $stringKey): void {
|
||||
if (!qdb_is_questionnaire_group_string_key($stringKey)) {
|
||||
return;
|
||||
}
|
||||
$path = qdb_app_string_catalog_path();
|
||||
if (!is_readable($path) || !is_writable($path)) {
|
||||
return;
|
||||
}
|
||||
$data = json_decode(file_get_contents($path), true);
|
||||
if (!is_array($data)) {
|
||||
return;
|
||||
}
|
||||
$changed = false;
|
||||
if (!empty($data['keys']) && is_array($data['keys'])) {
|
||||
$next = array_values(array_filter(
|
||||
$data['keys'],
|
||||
fn($k) => (string)$k !== $stringKey
|
||||
));
|
||||
if (count($next) !== count($data['keys'])) {
|
||||
$data['keys'] = $next;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
if (!empty($data['germanDefaults']) && is_array($data['germanDefaults']) && array_key_exists($stringKey, $data['germanDefaults'])) {
|
||||
unset($data['germanDefaults'][$stringKey]);
|
||||
$changed = true;
|
||||
}
|
||||
if (!$changed) {
|
||||
return;
|
||||
}
|
||||
file_put_contents(
|
||||
$path,
|
||||
json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n",
|
||||
LOCK_EX
|
||||
);
|
||||
qdb_app_string_catalog(true);
|
||||
}
|
||||
|
||||
function qdb_app_string_key_set(): array {
|
||||
$keys = qdb_app_string_catalog()['keys'] ?? [];
|
||||
return array_flip($keys);
|
||||
@ -299,6 +342,204 @@ function qdb_dev_only_app_string_key_set(): array {
|
||||
return $set;
|
||||
}
|
||||
|
||||
/** Prefix for app UI strings that label questionnaire category groups on the opening screen. */
|
||||
function qdb_questionnaire_group_prefix(): string {
|
||||
return 'questionnaire_group_';
|
||||
}
|
||||
|
||||
function qdb_questionnaire_group_string_key(string $categoryKey): string {
|
||||
$categoryKey = trim($categoryKey);
|
||||
return $categoryKey === '' ? '' : qdb_questionnaire_group_prefix() . $categoryKey;
|
||||
}
|
||||
|
||||
function qdb_is_questionnaire_group_string_key(string $stringKey): bool {
|
||||
return str_starts_with($stringKey, qdb_questionnaire_group_prefix());
|
||||
}
|
||||
|
||||
function qdb_category_key_from_group_string_key(string $stringKey): string {
|
||||
$prefix = qdb_questionnaire_group_prefix();
|
||||
return str_starts_with($stringKey, $prefix) ? substr($stringKey, strlen($prefix)) : '';
|
||||
}
|
||||
|
||||
/** @return array<string, int> categoryKey => questionnaire count */
|
||||
function qdb_category_keys_in_use(PDO $pdo): array {
|
||||
$map = [];
|
||||
$rows = $pdo->query(
|
||||
"SELECT categoryKey, COUNT(*) AS c FROM questionnaire
|
||||
WHERE trim(categoryKey) != '' GROUP BY categoryKey"
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($rows as $row) {
|
||||
$k = trim((string)($row['categoryKey'] ?? ''));
|
||||
if ($k !== '') {
|
||||
$map[$k] = (int)$row['c'];
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* App string entries for category keys in use (not already in the static JSON catalog keys list).
|
||||
* @return list<array{key: string, displayKey: string, type: string, entityId: string, sortOrder: int, germanDefault: string, categoryKey: string}>
|
||||
*/
|
||||
function qdb_category_group_string_entries(PDO $pdo): array {
|
||||
$catalog = qdb_app_string_catalog();
|
||||
$defaults = $catalog['germanDefaults'] ?? [];
|
||||
$staticKeys = qdb_app_string_key_set();
|
||||
$entries = [];
|
||||
$order = 0;
|
||||
foreach (qdb_category_keys_in_use($pdo) as $catKey => $_count) {
|
||||
$stringKey = qdb_questionnaire_group_string_key($catKey);
|
||||
if ($stringKey === '' || isset($staticKeys[$stringKey])) {
|
||||
continue;
|
||||
}
|
||||
$entries[] = [
|
||||
'key' => $stringKey,
|
||||
'displayKey' => $stringKey,
|
||||
'type' => 'app_string',
|
||||
'entityId' => $stringKey,
|
||||
'sortOrder' => $order++,
|
||||
'germanDefault' => $defaults[$stringKey] ?? $catKey,
|
||||
'categoryKey' => $catKey,
|
||||
];
|
||||
}
|
||||
usort($entries, fn($a, $b) => strcmp($a['categoryKey'], $b['categoryKey']));
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Category keys with translations in DB but no questionnaire using them.
|
||||
* @return list<string>
|
||||
*/
|
||||
function qdb_orphaned_category_keys(PDO $pdo): array {
|
||||
$inUse = qdb_category_keys_in_use($pdo);
|
||||
$prefix = qdb_questionnaire_group_prefix();
|
||||
$orphans = [];
|
||||
$stmt = $pdo->query(
|
||||
"SELECT DISTINCT stringKey FROM string_translation WHERE stringKey LIKE " . $pdo->quote($prefix . '%')
|
||||
);
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $stringKey) {
|
||||
$catKey = qdb_category_key_from_group_string_key((string)$stringKey);
|
||||
if ($catKey !== '' && !isset($inUse[$catKey])) {
|
||||
$orphans[$catKey] = true;
|
||||
}
|
||||
}
|
||||
ksort($orphans);
|
||||
return array_keys($orphans);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{categoryKey: string, stringKey: string, questionnaireCount: int, orphaned: bool, germanLabel: string}>
|
||||
*/
|
||||
function qdb_list_category_keys_for_dashboard(PDO $pdo): array {
|
||||
$defaults = qdb_app_string_catalog()['germanDefaults'] ?? [];
|
||||
$inUse = qdb_category_keys_in_use($pdo);
|
||||
$list = [];
|
||||
|
||||
$loadGerman = function (string $stringKey) use ($pdo, $defaults): string {
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT text FROM string_translation WHERE stringKey = :k AND languageCode = :lang LIMIT 1'
|
||||
);
|
||||
$stmt->execute([':k' => $stringKey, ':lang' => QDB_SOURCE_LANGUAGE]);
|
||||
$text = $stmt->fetchColumn();
|
||||
if (is_string($text) && $text !== '') {
|
||||
return $text;
|
||||
}
|
||||
return $defaults[$stringKey] ?? '';
|
||||
};
|
||||
|
||||
foreach ($inUse as $catKey => $count) {
|
||||
$stringKey = qdb_questionnaire_group_string_key($catKey);
|
||||
$list[] = [
|
||||
'categoryKey' => $catKey,
|
||||
'stringKey' => $stringKey,
|
||||
'questionnaireCount' => $count,
|
||||
'orphaned' => false,
|
||||
'germanLabel' => $loadGerman($stringKey) ?: $catKey,
|
||||
];
|
||||
}
|
||||
|
||||
foreach (qdb_orphaned_category_keys($pdo) as $catKey) {
|
||||
$stringKey = qdb_questionnaire_group_string_key($catKey);
|
||||
$list[] = [
|
||||
'categoryKey' => $catKey,
|
||||
'stringKey' => $stringKey,
|
||||
'questionnaireCount' => 0,
|
||||
'orphaned' => true,
|
||||
'germanLabel' => $loadGerman($stringKey) ?: $catKey,
|
||||
];
|
||||
}
|
||||
|
||||
usort($list, fn($a, $b) => strcmp($a['categoryKey'], $b['categoryKey']));
|
||||
return $list;
|
||||
}
|
||||
|
||||
/** Persist default German for a questionnaire_group_* key in app_ui_strings.json. */
|
||||
function qdb_update_questionnaire_group_german_default(string $stringKey, string $text): void {
|
||||
if (!qdb_is_questionnaire_group_string_key($stringKey)) {
|
||||
return;
|
||||
}
|
||||
$path = qdb_app_string_catalog_path();
|
||||
if (!is_readable($path) || !is_writable($path)) {
|
||||
return;
|
||||
}
|
||||
$data = json_decode(file_get_contents($path), true);
|
||||
if (!is_array($data)) {
|
||||
return;
|
||||
}
|
||||
if (!isset($data['germanDefaults']) || !is_array($data['germanDefaults'])) {
|
||||
$data['germanDefaults'] = [];
|
||||
}
|
||||
if (($data['germanDefaults'][$stringKey] ?? '') === $text) {
|
||||
return;
|
||||
}
|
||||
$data['germanDefaults'][$stringKey] = $text;
|
||||
file_put_contents(
|
||||
$path,
|
||||
json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n",
|
||||
LOCK_EX
|
||||
);
|
||||
qdb_app_string_catalog(true);
|
||||
}
|
||||
|
||||
/** Set German label for a category (app opening screen group title). */
|
||||
function qdb_set_category_german_label(PDO $pdo, string $categoryKey, string $text): string {
|
||||
$categoryKey = trim($categoryKey);
|
||||
if ($categoryKey === '') {
|
||||
json_error('INVALID_FIELD', 'categoryKey is required', 400);
|
||||
}
|
||||
$text = trim($text);
|
||||
if ($text === '') {
|
||||
json_error('INVALID_FIELD', 'German label is required', 400);
|
||||
}
|
||||
$stringKey = qdb_questionnaire_group_string_key($categoryKey);
|
||||
qdb_put_translation($pdo, 'app_string', $stringKey, QDB_SOURCE_LANGUAGE, $text);
|
||||
qdb_update_questionnaire_group_german_default($stringKey, $text);
|
||||
return $stringKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a category key completely: clear questionnaires, translations, and catalog defaults.
|
||||
* @return int questionnaires updated
|
||||
*/
|
||||
function qdb_delete_category_key(PDO $pdo, string $categoryKey): int {
|
||||
$categoryKey = trim($categoryKey);
|
||||
if ($categoryKey === '') {
|
||||
json_error('INVALID_FIELD', 'categoryKey is required', 400);
|
||||
}
|
||||
$stringKey = qdb_questionnaire_group_string_key($categoryKey);
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE questionnaire SET categoryKey = \'\' WHERE categoryKey = :ck');
|
||||
$stmt->execute([':ck' => $categoryKey]);
|
||||
$cleared = $stmt->rowCount();
|
||||
|
||||
$pdo->prepare('DELETE FROM string_translation WHERE stringKey = :k')
|
||||
->execute([':k' => $stringKey]);
|
||||
|
||||
qdb_remove_questionnaire_group_from_catalog($stringKey);
|
||||
|
||||
return $cleared;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global app UI strings (screens, toasts, auth, etc.) for the mobile app.
|
||||
* @return list<array{key: string, type: string, entityId: string, sortOrder: int}>
|
||||
@ -309,6 +550,9 @@ function qdb_app_ui_string_entries(): array {
|
||||
$entries = [];
|
||||
$order = 0;
|
||||
foreach ($catalog['keys'] ?? [] as $key) {
|
||||
if (qdb_is_questionnaire_group_string_key($key)) {
|
||||
continue;
|
||||
}
|
||||
$entries[] = [
|
||||
'key' => $key,
|
||||
'displayKey' => $key,
|
||||
@ -321,6 +565,20 @@ function qdb_app_ui_string_entries(): array {
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static catalog plus questionnaire_group_* keys for categoryKey values in use.
|
||||
* @return list<array{key: string, type: string, entityId: string, sortOrder: int}>
|
||||
*/
|
||||
function qdb_app_ui_string_entries_merged(PDO $pdo): array {
|
||||
$merged = qdb_app_ui_string_entries();
|
||||
$order = count($merged);
|
||||
foreach (qdb_category_group_string_entries($pdo) as $e) {
|
||||
$e['sortOrder'] = $order++;
|
||||
$merged[] = $e;
|
||||
}
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* All string keys referenced by questionnaire content (questions, options, config).
|
||||
* @return array<string, true>
|
||||
@ -384,10 +642,10 @@ function qdb_all_questionnaire_translation_key_set(PDO $pdo): array {
|
||||
* App UI catalog for the website Translations page (coach-visible, no dev-only keys).
|
||||
* Catalog keys always appear here even when also used inside a questionnaire (e.g. "save").
|
||||
*/
|
||||
function qdb_app_ui_string_entries_for_website(): array {
|
||||
function qdb_app_ui_string_entries_for_website(PDO $pdo): array {
|
||||
$dev = qdb_dev_only_app_string_key_set();
|
||||
$entries = [];
|
||||
foreach (qdb_app_ui_string_entries() as $e) {
|
||||
foreach (qdb_app_ui_string_entries_merged($pdo) as $e) {
|
||||
if (!isset($dev[$e['key']])) {
|
||||
$entries[] = $e;
|
||||
}
|
||||
@ -913,7 +1171,7 @@ function qdb_load_translations_for_entries(PDO $pdo, array $entries): array {
|
||||
* @return array<string, array<string, string>>
|
||||
*/
|
||||
function qdb_build_app_translations_map(PDO $pdo): array {
|
||||
$entries = qdb_app_ui_string_entries();
|
||||
$entries = qdb_app_ui_string_entries_merged($pdo);
|
||||
$translations = qdb_load_translations_for_entries($pdo, $entries);
|
||||
$defaults = qdb_app_string_catalog()['germanDefaults'] ?? [];
|
||||
$result = [];
|
||||
@ -1397,7 +1655,7 @@ function qdb_export_translations_bundle(PDO $pdo): array {
|
||||
qdb_ensure_source_language($pdo);
|
||||
|
||||
$exportEntries = [];
|
||||
$appEntries = qdb_app_ui_string_entries_for_website();
|
||||
$appEntries = qdb_app_ui_string_entries_for_website($pdo);
|
||||
$appTranslations = qdb_load_translations_for_entries($pdo, $appEntries);
|
||||
qdb_attach_translation_texts($appEntries, $appTranslations);
|
||||
foreach ($appEntries as $e) {
|
||||
|
||||
Reference in New Issue
Block a user