Skip to content

Commit 3aa5a1d

Browse files
committed
User Preferences date formats. Account Access / Transactions
1 parent 7384484 commit 3aa5a1d

11 files changed

Lines changed: 1172 additions & 176 deletions

File tree

src/lib/components/LightSwitch.svelte

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import { Portal, Switch, Tooltip } from '@skeletonlabs/skeleton-svelte';
33
import { onMount } from 'svelte';
4+
import { userPreferences } from '$lib/stores/userPreferences.svelte';
45
56
// Icons
67
import IconMoon from '@lucide/svelte/icons/moon';
@@ -16,15 +17,16 @@
1617
let toolTipString = $derived(checked ? 'Toggle Light Mode' : 'Toggle Dark Mode');
1718
1819
onMount(() => {
19-
const storedMode = localStorage.getItem('mode') || 'dark';
20-
mode = storedMode as 'dark' | 'light';
20+
// Read from preferences store (which reads from localStorage / OBP)
21+
mode = userPreferences.theme;
2122
document.documentElement.setAttribute('data-mode', mode);
2223
});
2324
2425
const onCheckedChange = (details: { checked: boolean }) => {
2526
mode = details.checked ? 'dark' : 'light';
2627
document.documentElement.setAttribute('data-mode', mode);
27-
localStorage.setItem('mode', mode);
28+
// Save via preferences store (syncs to localStorage + OBP)
29+
userPreferences.setTheme(mode);
2830
};
2931
</script>
3032

src/lib/config/navigation.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,11 @@ function buildAbacItems(): NavigationItem[] {
515515
label: "Rules",
516516
iconComponent: Lock,
517517
},
518+
{
519+
href: "/users?role_name=CanExecuteAbacRule",
520+
label: "ABAC Users",
521+
iconComponent: Users,
522+
},
518523
];
519524

520525
return items;
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { browser } from "$app/environment";
2+
import { createLogger } from "$lib/utils/logger";
3+
import { trackedFetch } from "$lib/utils/trackedFetch";
4+
5+
const logger = createLogger("UserPreferences");
6+
7+
const STORAGE_KEY = "userPreferences";
8+
9+
export type DateFormat = "DD/MM/YYYY" | "MM/DD/YYYY" | "YYYY-MM-DD";
10+
export type Theme = "dark" | "light";
11+
12+
export const DATE_FORMAT_OPTIONS: { value: DateFormat; label: string }[] = [
13+
{ value: "DD/MM/YYYY", label: "DD/MM/YYYY (31/12/2025)" },
14+
{ value: "MM/DD/YYYY", label: "MM/DD/YYYY (12/31/2025)" },
15+
{ value: "YYYY-MM-DD", label: "YYYY-MM-DD (2025-12-31)" },
16+
];
17+
18+
export const THEME_OPTIONS: { value: Theme; label: string }[] = [
19+
{ value: "dark", label: "Dark" },
20+
{ value: "light", label: "Light" },
21+
];
22+
23+
interface PersonalDataField {
24+
user_attribute_id: string;
25+
name: string;
26+
type: string;
27+
value: string;
28+
is_personal: boolean;
29+
insert_date?: string;
30+
}
31+
32+
interface StoredPreferences {
33+
dateFormat: DateFormat;
34+
theme: Theme;
35+
dateFormatAttributeId?: string;
36+
themeAttributeId?: string;
37+
}
38+
39+
const FIELD_DATE_FORMAT = "DATE_FORMAT";
40+
const FIELD_THEME = "THEME";
41+
42+
class UserPreferencesStore {
43+
dateFormat = $state<DateFormat>("DD/MM/YYYY");
44+
theme = $state<Theme>("dark");
45+
loading = $state(false);
46+
private dateFormatAttributeId: string | null = null;
47+
private themeAttributeId: string | null = null;
48+
private initialized = false;
49+
50+
constructor() {
51+
if (browser) {
52+
this.loadFromLocalStorage();
53+
}
54+
}
55+
56+
private loadFromLocalStorage() {
57+
try {
58+
const stored = localStorage.getItem(STORAGE_KEY);
59+
if (stored) {
60+
const prefs: StoredPreferences = JSON.parse(stored);
61+
this.dateFormat = prefs.dateFormat || "DD/MM/YYYY";
62+
this.theme = prefs.theme || "dark";
63+
this.dateFormatAttributeId = prefs.dateFormatAttributeId || null;
64+
this.themeAttributeId = prefs.themeAttributeId || null;
65+
logger.info(`Restored preferences from localStorage: dateFormat=${this.dateFormat}, theme=${this.theme}`);
66+
} else {
67+
// Fall back to the legacy theme storage
68+
const legacyTheme = localStorage.getItem("mode");
69+
if (legacyTheme === "light" || legacyTheme === "dark") {
70+
this.theme = legacyTheme;
71+
logger.info(`Restored theme from legacy localStorage: ${this.theme}`);
72+
}
73+
}
74+
} catch (e) {
75+
logger.error("Failed to restore preferences from localStorage:", e);
76+
}
77+
}
78+
79+
private saveToLocalStorage() {
80+
if (!browser) return;
81+
try {
82+
const prefs: StoredPreferences = {
83+
dateFormat: this.dateFormat,
84+
theme: this.theme,
85+
dateFormatAttributeId: this.dateFormatAttributeId || undefined,
86+
themeAttributeId: this.themeAttributeId || undefined,
87+
};
88+
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
89+
// Keep legacy theme key in sync for LightSwitch compatibility
90+
localStorage.setItem("mode", this.theme);
91+
} catch (e) {
92+
logger.error("Failed to save preferences to localStorage:", e);
93+
}
94+
}
95+
96+
/** Load preferences from OBP personal data fields */
97+
async loadFromOBP() {
98+
if (!browser) return;
99+
this.loading = true;
100+
try {
101+
const res = await trackedFetch("/api/user/preferences");
102+
if (!res.ok) {
103+
logger.warn("Failed to load preferences from OBP:", res.status);
104+
return;
105+
}
106+
const data = await res.json();
107+
const attributes: PersonalDataField[] = data.user_attributes || [];
108+
109+
for (const attr of attributes) {
110+
if (attr.name === FIELD_DATE_FORMAT) {
111+
this.dateFormat = (attr.value as DateFormat) || "DD/MM/YYYY";
112+
this.dateFormatAttributeId = attr.user_attribute_id;
113+
} else if (attr.name === FIELD_THEME) {
114+
this.theme = (attr.value as Theme) || "dark";
115+
this.themeAttributeId = attr.user_attribute_id;
116+
// Apply theme immediately
117+
document.documentElement.setAttribute("data-mode", this.theme);
118+
}
119+
}
120+
121+
this.saveToLocalStorage();
122+
this.initialized = true;
123+
logger.info(`Loaded preferences from OBP: dateFormat=${this.dateFormat}, theme=${this.theme}`);
124+
} catch (e) {
125+
logger.error("Error loading preferences from OBP:", e);
126+
} finally {
127+
this.loading = false;
128+
}
129+
}
130+
131+
/** Save a single preference to OBP (create or update) */
132+
private async saveToOBP(name: string, value: string, attributeId: string | null): Promise<string | null> {
133+
if (!browser) return null;
134+
try {
135+
if (attributeId) {
136+
// Update existing
137+
const res = await trackedFetch("/api/user/preferences", {
138+
method: "PUT",
139+
headers: { "Content-Type": "application/json" },
140+
body: JSON.stringify({
141+
user_attribute_id: attributeId,
142+
name,
143+
value,
144+
type: "STRING",
145+
}),
146+
});
147+
if (!res.ok) {
148+
logger.error(`Failed to update preference ${name}:`, res.status);
149+
return attributeId;
150+
}
151+
const data = await res.json();
152+
return data.user_attribute_id || attributeId;
153+
} else {
154+
// Create new
155+
const res = await trackedFetch("/api/user/preferences", {
156+
method: "POST",
157+
headers: { "Content-Type": "application/json" },
158+
body: JSON.stringify({
159+
name,
160+
value,
161+
type: "STRING",
162+
}),
163+
});
164+
if (!res.ok) {
165+
logger.error(`Failed to create preference ${name}:`, res.status);
166+
return null;
167+
}
168+
const data = await res.json();
169+
return data.user_attribute_id || null;
170+
}
171+
} catch (e) {
172+
logger.error(`Error saving preference ${name} to OBP:`, e);
173+
return attributeId;
174+
}
175+
}
176+
177+
async setDateFormat(format: DateFormat) {
178+
this.dateFormat = format;
179+
this.saveToLocalStorage();
180+
this.dateFormatAttributeId = await this.saveToOBP(FIELD_DATE_FORMAT, format, this.dateFormatAttributeId);
181+
this.saveToLocalStorage();
182+
}
183+
184+
async setTheme(theme: Theme) {
185+
this.theme = theme;
186+
// Apply theme immediately
187+
if (browser) {
188+
document.documentElement.setAttribute("data-mode", theme);
189+
}
190+
this.saveToLocalStorage();
191+
this.themeAttributeId = await this.saveToOBP(FIELD_THEME, theme, this.themeAttributeId);
192+
this.saveToLocalStorage();
193+
}
194+
195+
/** Format a date string according to the user's preferred date format */
196+
formatDate(dateString: string | undefined | null): string {
197+
if (!dateString) return "\u2014";
198+
try {
199+
const date = new Date(dateString);
200+
if (isNaN(date.getTime())) return "\u2014";
201+
202+
const day = String(date.getDate()).padStart(2, "0");
203+
const month = String(date.getMonth() + 1).padStart(2, "0");
204+
const year = date.getFullYear();
205+
206+
switch (this.dateFormat) {
207+
case "DD/MM/YYYY":
208+
return `${day}/${month}/${year}`;
209+
case "MM/DD/YYYY":
210+
return `${month}/${day}/${year}`;
211+
case "YYYY-MM-DD":
212+
return `${year}-${month}-${day}`;
213+
default:
214+
return `${day}/${month}/${year}`;
215+
}
216+
} catch {
217+
return "\u2014";
218+
}
219+
}
220+
221+
clear() {
222+
this.dateFormat = "DD/MM/YYYY";
223+
this.theme = "dark";
224+
this.dateFormatAttributeId = null;
225+
this.themeAttributeId = null;
226+
this.initialized = false;
227+
if (browser) {
228+
localStorage.removeItem(STORAGE_KEY);
229+
}
230+
logger.info("Cleared user preferences");
231+
}
232+
}
233+
234+
export const userPreferences = new UserPreferencesStore();

0 commit comments

Comments
 (0)