Initial Version
This commit is contained in:
6
core/src/composables/index.ts
Normal file
6
core/src/composables/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Core composables - reusable composition functions
|
||||
*/
|
||||
|
||||
export { useClipboard } from './useClipboard'
|
||||
export { usePreferences } from './usePreferences'
|
||||
45
core/src/composables/useClipboard.ts
Normal file
45
core/src/composables/useClipboard.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
/**
|
||||
* Composable for clipboard operations with visual feedback
|
||||
*
|
||||
* @param timeout - Duration in ms to show success state (default: 2000)
|
||||
* @returns Object containing copiedKey ref and copyToClipboard function
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup>
|
||||
* import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||
*
|
||||
* const { copiedKey, copyToClipboard } = useClipboard()
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <v-btn
|
||||
* :icon="copiedKey === index ? 'mdi-check' : 'mdi-content-copy'"
|
||||
* :color="copiedKey === index ? 'success' : undefined"
|
||||
* @click="copyToClipboard(text, index)"
|
||||
* />
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useClipboard<T = number>(timeout = 2000) {
|
||||
const copiedKey = ref<T | null>(null)
|
||||
|
||||
const copyToClipboard = async (text: string, key: T): Promise<boolean> => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
copiedKey.value = key
|
||||
setTimeout(() => { copiedKey.value = null }, timeout)
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Failed to copy to clipboard:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
copiedKey,
|
||||
copyToClipboard
|
||||
}
|
||||
}
|
||||
208
core/src/composables/usePreferences.ts
Normal file
208
core/src/composables/usePreferences.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { usePreferencesStore, type PreferencesState } from '@KTXC/stores/preferencesStore';
|
||||
import { preferenceService } from '@KTXC/services/preferenceService';
|
||||
|
||||
/**
|
||||
* Composable for managing user preferences
|
||||
* Provides reactive access to preferences with automatic sync to server
|
||||
*/
|
||||
export function usePreferences() {
|
||||
const store = usePreferencesStore();
|
||||
const saving = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
/**
|
||||
* Get all preferences
|
||||
*/
|
||||
const preferences = computed(() => store.preferences);
|
||||
|
||||
/**
|
||||
* Get locked preference keys
|
||||
*/
|
||||
const locks = computed(() => store.locks);
|
||||
|
||||
/**
|
||||
* Check if a preference is locked by tenant admin
|
||||
*/
|
||||
const isLocked = (key: keyof PreferencesState): boolean => {
|
||||
return store.isLocked(key);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a single preference value
|
||||
*/
|
||||
const get = <K extends keyof PreferencesState>(key: K): PreferencesState[K] => {
|
||||
return store.getPreference(key);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a single preference and sync to server
|
||||
*/
|
||||
const set = async <K extends keyof PreferencesState>(
|
||||
key: K,
|
||||
value: PreferencesState[K],
|
||||
syncToServer = true
|
||||
): Promise<boolean> => {
|
||||
error.value = null;
|
||||
|
||||
// Update local state first
|
||||
const success = store.setPreference(key, value);
|
||||
if (!success) {
|
||||
error.value = `Preference "${key}" is locked by administrator`;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sync to server if requested
|
||||
if (syncToServer) {
|
||||
saving.value = true;
|
||||
try {
|
||||
const response = await preferenceService.setPreference(key, value);
|
||||
// Update store with server response to ensure consistency
|
||||
store.setPreferences(response.effective);
|
||||
store.setLocks(response.locks);
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to save preference';
|
||||
return false;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update multiple preferences and sync to server
|
||||
*/
|
||||
const update = async (
|
||||
prefs: Partial<PreferencesState>,
|
||||
syncToServer = true
|
||||
): Promise<{ success: boolean; rejected: string[] }> => {
|
||||
error.value = null;
|
||||
|
||||
// Update local state
|
||||
store.setPreferences(prefs);
|
||||
|
||||
if (syncToServer) {
|
||||
saving.value = true;
|
||||
try {
|
||||
const response = await preferenceService.updatePreferences(prefs);
|
||||
store.setPreferences(response.effective);
|
||||
store.setLocks(response.locks);
|
||||
return {
|
||||
success: true,
|
||||
rejected: response.rejectedKeys ?? [],
|
||||
};
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to save preferences';
|
||||
return { success: false, rejected: [] };
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, rejected: [] };
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset preferences to tenant defaults
|
||||
*/
|
||||
const reset = async (): Promise<boolean> => {
|
||||
error.value = null;
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
const response = await preferenceService.resetPreferences();
|
||||
store.setPreferences(response.effective);
|
||||
store.setLocks(response.locks);
|
||||
return true;
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to reset preferences';
|
||||
return false;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh preferences from server
|
||||
*/
|
||||
const refresh = async (): Promise<boolean> => {
|
||||
error.value = null;
|
||||
store.setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await preferenceService.getPreferences();
|
||||
store.setPreferences(response.effective);
|
||||
store.setLocks(response.locks);
|
||||
return true;
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to load preferences';
|
||||
return false;
|
||||
} finally {
|
||||
store.setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Individual preference computed refs for convenience
|
||||
const theme = computed({
|
||||
get: () => store.preferences.theme,
|
||||
set: (value: string) => set('theme', value),
|
||||
});
|
||||
|
||||
const language = computed({
|
||||
get: () => store.preferences.language,
|
||||
set: (value: string) => set('language', value),
|
||||
});
|
||||
|
||||
const timezone = computed({
|
||||
get: () => store.preferences.timezone,
|
||||
set: (value: string) => set('timezone', value),
|
||||
});
|
||||
|
||||
const dateFormat = computed({
|
||||
get: () => store.preferences.date_format,
|
||||
set: (value: string) => set('date_format', value),
|
||||
});
|
||||
|
||||
const timeFormat = computed({
|
||||
get: () => store.preferences.time_format,
|
||||
set: (value: string) => set('time_format', value),
|
||||
});
|
||||
|
||||
const weekStart = computed({
|
||||
get: () => store.preferences.week_start,
|
||||
set: (value: string) => set('week_start', value),
|
||||
});
|
||||
|
||||
const defaultModule = computed({
|
||||
get: () => store.preferences.default_module ?? '',
|
||||
set: (value: string) => set('default_module', value),
|
||||
});
|
||||
|
||||
return {
|
||||
// State
|
||||
preferences,
|
||||
locks,
|
||||
saving,
|
||||
error,
|
||||
loading: computed(() => store.loading),
|
||||
|
||||
// Methods
|
||||
get,
|
||||
set,
|
||||
update,
|
||||
reset,
|
||||
refresh,
|
||||
isLocked,
|
||||
|
||||
// Individual preference refs
|
||||
theme,
|
||||
language,
|
||||
timezone,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
weekStart,
|
||||
defaultModule,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user