9 Commits

Author SHA1 Message Date
Adrian Zürcher
15f9026a5f new release
All checks were successful
Build Quasar SPA and Go Backend for memberApp / build-spa (push) Successful in 8m11s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, .exe, windows) (push) Successful in 7m27s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, , linux) (push) Successful in 7m42s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm64, , linux) (push) Successful in 6m45s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm, 6, , linux) (push) Successful in 6m59s
2026-02-19 18:20:43 +01:00
Adrian Zürcher
e25bac1a1e fix widget size in phone 2026-02-19 18:19:38 +01:00
Adrian Zürcher
548cd9d622 fix pdf size in phone and on desktop close #52 2026-02-19 18:18:58 +01:00
Adrian Zürcher
8e3e8f8bc7 chnage role table so own user can not change his own rules 2026-02-19 10:51:32 +01:00
Adrian Zürcher
e686a27bf1 change username to user 2026-02-19 10:51:09 +01:00
Adrian Zürcher
ab88acd740 add new openDatabse function 2026-02-19 10:50:40 +01:00
Adrian Zürcher
6392877dc1 add new workspaces for users 2026-02-19 10:49:12 +01:00
Adrian Zürcher
b726eb42dc add filter so user can not change to admin if the have the right to change user rights 2026-02-14 13:43:21 +01:00
Adrian Zürcher
8963cba016 fix update role not working 2026-02-14 13:42:54 +01:00
36 changed files with 857 additions and 138 deletions

View File

@@ -3,9 +3,9 @@ module backend
go 1.25.4
require (
gitea.tecamino.com/paadi/access-handler v1.0.34
gitea.tecamino.com/paadi/memberDB v1.1.28
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1
gitea.tecamino.com/paadi/access-handler v1.0.48
gitea.tecamino.com/paadi/memberDB v1.1.30
gitea.tecamino.com/paadi/tecamino-dbm v1.0.0
gitea.tecamino.com/paadi/tecamino-logger v0.2.1
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.11.0
@@ -14,7 +14,7 @@ require (
)
require (
gitea.tecamino.com/paadi/dbHandler v1.1.11 // indirect
gitea.tecamino.com/paadi/dbHandler v1.1.12 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect

View File

@@ -1,11 +1,11 @@
gitea.tecamino.com/paadi/access-handler v1.0.34 h1:6P65HiusSfvgv/ezOvxSahqyRJMK9UrxtGsz6loLoUk=
gitea.tecamino.com/paadi/access-handler v1.0.34/go.mod h1:HyMp1WvzmqLw8Ljt3r1qlF8fY+T5WFXr9Da/CTIM0H8=
gitea.tecamino.com/paadi/dbHandler v1.1.11 h1:hTpMWRr4dW7TkiBnEku0/3ggDC7/uP82U9paRKY/QEs=
gitea.tecamino.com/paadi/dbHandler v1.1.11/go.mod h1:y/xn/POJg1DO++67uKvnO23lJQgh+XFQq7HZCS9Getw=
gitea.tecamino.com/paadi/memberDB v1.1.28 h1:QSgPFIvzWS17bAIHp01nqUG5CQuE74AckrdYg6xZljw=
gitea.tecamino.com/paadi/memberDB v1.1.28/go.mod h1:uLoKel+EcuXUzxAY5ugfWh640TSomfTJR+g8Jfe8YKI=
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1 h1:vAq7mwUxlxJuLzCQSDMrZCwo8ky5usWi9Qz+UP+WnkI=
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1/go.mod h1:+tmf1rjPaKEoNeUcr1vdtoFIFweNG3aUGevDAl3NMBk=
gitea.tecamino.com/paadi/access-handler v1.0.48 h1:PYZvOwR9HCORAFpm7Nd5ZXvWwT5w04OvbcHhVHmPJlw=
gitea.tecamino.com/paadi/access-handler v1.0.48/go.mod h1:0kUGU4Jw2jSvopCCwecuX/2QnVKS09Ec1KQNrBXvsFs=
gitea.tecamino.com/paadi/dbHandler v1.1.12 h1:F1ARSTUm0MZmF84FfD/g5RQNMYyDYXHYrB3cXPSi4qw=
gitea.tecamino.com/paadi/dbHandler v1.1.12/go.mod h1:y/xn/POJg1DO++67uKvnO23lJQgh+XFQq7HZCS9Getw=
gitea.tecamino.com/paadi/memberDB v1.1.30 h1:N+3V9A/+OAGIoJeUNVHj1qUuBcy6ADLYFIgCnp2Ggk4=
gitea.tecamino.com/paadi/memberDB v1.1.30/go.mod h1:Q4NO1cdBm/6RLF+bP2NEzBPJURKjyIr4u3dElDXmHWI=
gitea.tecamino.com/paadi/tecamino-dbm v1.0.0 h1:xFgcpIiQMyqbglScZBAbdOQyM+yOJ3GHMK2iX5Ep3Gg=
gitea.tecamino.com/paadi/tecamino-dbm v1.0.0/go.mod h1:+tmf1rjPaKEoNeUcr1vdtoFIFweNG3aUGevDAl3NMBk=
gitea.tecamino.com/paadi/tecamino-logger v0.2.1 h1:sQTBKYPdzn9mmWX2JXZBtGBvNQH7cuXIwsl4TD0aMgE=
gitea.tecamino.com/paadi/tecamino-logger v0.2.1/go.mod h1:FkzRTldUBBOd/iy2upycArDftSZ5trbsX5Ira5OzJgM=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=

View File

@@ -145,6 +145,7 @@ func main() {
auth.GET("/users", accessHandler.GetUser)
auth.GET("/roles", accessHandler.GetRole)
auth.GET("/workspaces", accessHandler.GetWorkspace)
auth.POST("database/open", dbHandler.OpenDatabase)
auth.POST("/members/add", dbHandler.AddNewMember)
@@ -177,6 +178,11 @@ func main() {
auth.POST("/users/new/password", accessHandler.ChangePassword)
auth.POST("/users/delete", accessHandler.DeleteUser)
auth.POST("/workspaces/add", accessHandler.AddWorkspace)
auth.POST("/workspaces/update", accessHandler.UpdateWorkspace)
auth.POST("/workspaces/data", accessHandler.ReadWorkspaceData)
auth.POST("/workspaces/delete", accessHandler.DeleteWorkspace)
api.POST("/login/refresh", accessHandler.Refresh)
// Serve static files
@@ -228,4 +234,8 @@ func main() {
if err := s.ServeHttp(env.HostUrl.GetValue(), env.HostPort.GetUIntValue()); err != nil {
logger.Error("main", "error http server "+err.Error())
}
if err := s.ServeHttp(env.HostUrl.GetValue(), env.HostPort.GetUIntValue()); err != nil {
logger.Error("main", "error http server "+err.Error())
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "lightcontrol",
"version": "1.3.2",
"version": "1.4.0",
"description": "A Tecamino App",
"productName": "Attendence Records",
"author": "A. Zuercher",

View File

@@ -197,3 +197,11 @@ calendar:
firstDayOfWeek: 1
format24h: true
pluralDay: 'Täg'
description: Beschribig
workspace: Workspace
workspaces: Workspaces
addNewWorkspace: Füeg neuis Workspace hinzue
saved: gspicheret
noWorkspaceFound: Kes Workspace gfunge
addNewDatabase: Nei Datenbank
fileNeedsToEndWith: Dateiname mues fougendi endig ha

View File

@@ -65,8 +65,8 @@ roleIsRequired: Rolle ist erforderlich
permissions: Rechte
selectRoleOptions: Wähle Rollen Optionen
selectEventOptions: Wähle Veranstaltungs Optionen
addNewRole: Füge neue Rolle hinzu
addNewEvent: Füeg neue Veranstaltung hinzu
addNewRole: Neue Rolle hinzufügen
addNewEvent: Neue Veranstaltung hinzufügen
veryWeak: sehr Schwach
weak: Schwach
fair: Ausreichend
@@ -197,3 +197,11 @@ calendar:
firstDayOfWeek: 1
format24h: true
pluralDay: 'Tage'
description: Beschreibung
workspace: Workspace
workspaces: Workspaces
addNewWorkspace: Neues Workspace hinzufügen
saved: gespeichert
noWorkspaceFound: Kein Workspace gefunden
addNewDatabase: Neue Datenbank hinzufügen
fileNeedsToEndWith: Dateiname muss folgende Endung haben

View File

@@ -197,3 +197,11 @@ calendar:
firstDayOfWeek: 0
format24h: false
pluralDay: 'Days'
description: Description
workspace: Workspace
workspaces: Workspaces
addNewWorkspace: Add new Workspace
saved: saved
noWorkspaceFound: No Workspace found
addNewDatabase: Add new database
fileNeedsToEndWith: Filename must end with

View File

@@ -37,7 +37,7 @@ login: Iniciar sesión
logout: Cerrar sesión
user: Usuario
password: Contraseña
isRequired: Es obligatorio
isRequired: Obligatorio
colors: Colores
primaryColor: Color principal
primaryColorText: Color del texto principal
@@ -108,7 +108,7 @@ attendeeAdded: Asistente añadido
attendeesAdded: Asistentes añadidos
eventAdded: Evento añadido
userUpdated: Usuario actualizado
selectResponsibleOptions: Seleccionar opciones responsables
selectResponsibleOptions: Seleccionar opciones de responsables
addNewResponsible: Añadir responsable
responsibleAdded: Responsable añadido
responsiblesAdded: Responsables añadidos
@@ -197,3 +197,11 @@ calendar:
firstDayOfWeek: 1
format24h: true
pluralDay: 'dias'
description: Descripción
workspace: Workspace
workspaces: Workspaces
addNewWorkspace: Añadir nuevo Workspace
saved: guardado
noWorkspaceFound: No se encontró Workspace
addNewDatabase: Agregar nueva base de datos
fileNeedsToEndWith: El nombre del archivo debe terminar con

View File

@@ -17,7 +17,7 @@ export default boot(async ({ app }) => {
useStore
.setUser({
id: resp.data.id,
username: resp.data.username,
user: resp.data.username,
role: { role: resp.data.role, permissions: [] },
})
.catch((err) => console.error(err));

View File

@@ -1,7 +1,7 @@
import { boot } from 'quasar/wrappers';
import { setQuasarInstance } from 'src/vueLib/utils/globalQ';
import { setRouterInstance } from 'src/vueLib/utils/globalRouter';
import { databaseName, logo, appName } from 'src/vueLib/models/settings';
import { databaseName, logo, appName, workspace } from 'src/vueLib/models/settings';
import { Dark } from 'quasar';
import { getLocalDarkMode, getLocalSettings } from 'src/localstorage/localStorage';
@@ -20,7 +20,8 @@ export default boot(({ app, router }) => {
if (settings.appName) {
appName.value = settings.appName;
}
databaseName.value = settings.databaseName ?? databaseName.value;
databaseName.value = settings.databaseName;
workspace.value = settings.workspace?.uuid ?? '';
document.documentElement.style.setProperty('--q-primary', settings.primaryColor ?? '#1976d2');
document.documentElement.style.setProperty(

View File

@@ -30,6 +30,9 @@
class="col-5 required"
:label="$t('role')"
filled
option-label="role"
option-value="role"
emit-value
:options="props.roles"
:rules="[(val) => !!val || $t('roleIsRequired')]"
v-model="role"
@@ -66,6 +69,7 @@ const dialog = ref();
const form = ref();
const newUser = ref(false);
const role = ref('');
const localUser = ref<User>({
user: '',
email: '',

View File

@@ -0,0 +1,105 @@
<template>
<DialogFrame
ref="dialog"
:header-title="newWorkspace ? $t('addNewWorkspace') : $t('edit') + ' ' + localWorkspace.name"
:height="300"
:width="600"
>
<div class="column">
<div class="row justify-center">
<q-input
class="col-5 required"
:label="$t('workspace')"
filled
:rules="[(val) => !!val || $t('workspaceIsRequired')]"
v-model="localWorkspace.name"
autofocus
></q-input>
</div>
<div class="row justify-center">
<q-input
dense
class="col-5 required"
:label="$t('description')"
filled
v-model="localWorkspace.description"
></q-input>
</div>
</div>
<div class="row justify-center">
<q-btn class="q-ma-md" color="primary" no-caps @click="save">{{ $t('save') }}</q-btn>
</div>
</DialogFrame>
</template>
<script setup lang="ts">
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
import { ref } from 'vue';
import { appApi } from 'src/boot/axios';
import type { Workspace } from 'src/vueLib/models/workspaces';
import { useNotify } from 'src/vueLib/general/useNotify';
import { i18n } from 'src/boot/lang';
const { NotifyResponse } = useNotify();
const dialog = ref();
const newWorkspace = ref(false);
const localWorkspace = ref<Workspace>({
name: '',
description: '',
});
const emit = defineEmits(['update']);
function open(workspace: Workspace | null) {
if (workspace === undefined) {
return;
}
if (workspace !== null) {
localWorkspace.value = { ...workspace };
localWorkspace.value.description = workspace.description;
newWorkspace.value = false;
} else {
localWorkspace.value = {
name: '',
description: '',
};
newWorkspace.value = true;
}
dialog.value?.open();
}
async function save() {
let query = 'workspaces/update?id=' + localWorkspace.value.id;
let update = true;
if (newWorkspace.value) {
query = 'workspaces/add';
update = false;
}
await appApi
.post(query, JSON.stringify(localWorkspace.value))
.then(() => {
if (update) {
NotifyResponse(
i18n.global.t('workspace') +
" '" +
localWorkspace.value.name +
"' " +
i18n.global.t('updated'),
);
}
emit('update');
dialog.value.close();
})
.catch((err) => NotifyResponse(err, 'error'));
}
defineExpose({ open });
</script>
<style>
.required .q-field__label::after {
content: ' *';
color: red;
}
</style>

View File

@@ -1,11 +1,12 @@
import { Dark } from 'quasar';
import { appName, databaseName, type Settings } from 'src/vueLib/models/settings';
import { appName, databaseName, workspace, type Settings } from 'src/vueLib/models/settings';
import { ref } from 'vue';
export function setLocalSettings(settings: Settings) {
localStorage.setItem('icon', settings.icon);
localStorage.setItem('appName', settings.appName);
localStorage.setItem('databaseName', settings.databaseName);
localStorage.setItem('workspace', settings.workspace?.uuid || '');
localStorage.setItem('primaryColor', settings.primaryColor);
localStorage.setItem('primaryColorText', settings.primaryColorText);
localStorage.setItem('secondaryColor', settings.secondaryColor);
@@ -28,9 +29,15 @@ export function getLocalSettings(): Settings {
iconName = '';
}
let ws = localStorage.getItem('workspace');
if (ws === undefined || ws === 'undefined') {
ws = workspace.value || '';
}
return <Settings>{
icon: iconName,
appName: name,
workspace: { name: '', description: '', uuid: ws },
databaseName: db,
primaryColor: localStorage.getItem('primaryColor'),
primaryColorText: localStorage.getItem('primaryColorText'),
@@ -42,6 +49,7 @@ export function getLocalSettings(): Settings {
export function clearLocalStorage() {
localStorage.removeItem('icon');
localStorage.removeItem('appName');
localStorage.removeItem('workspace');
localStorage.removeItem('databaseName');
localStorage.removeItem('primaryColor');
localStorage.removeItem('primaryColorText');

View File

@@ -15,7 +15,7 @@ const router = useRouter();
const userStore = useUserStore();
onMounted(() => {
if (userStore.user?.username !== '' && userStore.user?.role.role !== '') {
if (userStore.user?.user !== '' && userStore.user?.role.role !== '') {
forwardToPage().catch((err) => console.error(err));
}
});

View File

@@ -82,8 +82,12 @@
v-if="attendees !== undefined"
:class="
nonAttendees !== undefined
? 'col-12 col-sm-5 col-md-5 q-pa-md'
: 'col-12 col-md-8 col-lg-5'
? printing
? 'col-5 q-pa-md'
: 'col-12 col-sm-5 col-md-5 q-pa-md'
: printing
? 'col-5'
: 'col-12 col-md-8 col-lg-5'
"
>
<q-table
@@ -103,7 +107,13 @@
<div
v-if="nonAttendees !== undefined"
:class="
attendees !== undefined ? 'col-12 col-sm-5 col-md-5 q-pa-md' : 'col-12 col-md-8 col-lg-5'
attendees !== undefined
? printing
? 'col-5 q-pa-md'
: 'col-12 col-sm-5 col-md-5 q-pa-md'
: printing
? 'col-5'
: 'col-12 col-md-8 col-lg-5'
"
>
<q-table
@@ -130,13 +140,14 @@ import DateDaySelect from 'src/components/DateDaySelect.vue';
import { computed, onMounted, ref } from 'vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { i18n } from 'src/boot/lang';
import { appName, databaseName } from 'src/vueLib/models/settings';
import { appName } from 'src/vueLib/models/settings';
import type { Amount } from 'src/vueLib/models/report';
import ReportStat from 'src/components/ReportStat.vue';
import type { Group, Groups } from 'src/vueLib/models/group';
import { getLocalPageDefaults, setLocalPageDefaults } from 'src/localstorage/localStorage';
import html2pdf from 'html2pdf.js';
import type { PageDefault } from 'src/vueLib/models/pageDefaults';
import { openDatabase } from 'src/vueLib/components/DatabaseCall';
const filter = ref<string>('');
const group = ref<Group[]>([]);
@@ -150,6 +161,7 @@ const loading = ref(false);
const amounts = ref<Amount[]>([]);
const reportExportRef = ref<HTMLElement | null>(null);
const weekdays = ref<number[]>([0, 3]);
const printing = ref<boolean>(false);
const columns = computed(() => [
{
@@ -168,10 +180,9 @@ const columns = computed(() => [
},
]);
onMounted(() => {
onMounted(async () => {
loading.value = true;
appApi
.post('database/open', { dbPath: databaseName.value })
await openDatabase()
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
@@ -268,10 +279,14 @@ function updateReport(dates: string[]) {
}
function printReport() {
printing.value = true;
window.print();
printing.value = false;
}
async function downloadPDF() {
printing.value = true;
const element = reportExportRef.value;
if (!element) return;
// Generate date string (YYYY-MM-DD)
@@ -308,7 +323,8 @@ async function downloadPDF() {
.save()
.catch((error) => {
console.error('PDF Generation failed:', error);
});
})
.finally(() => (printing.value = false));
}
</script>

View File

@@ -26,15 +26,57 @@
</q-card>
<q-card class="q-ma-lg">
<p class="text-bold text-h6 text-primary q-pa-md">{{ $t('database') }}</p>
<div class="row">
<q-input
<div
v-if="localUser?.workspaces !== undefined && localUser?.workspaces.length > 0"
class="row"
>
<q-select
dense
:readonly="!user.isPermittedTo('settings', 'write')"
:class="[colorGroup ? 'col-md-4' : 'col-md-12', 'q-pa-md']"
:class="[colorGroup ? 'col-md-4' : 'col-xs-12 col-sm-6 col-md-12', 'q-pa-md']"
filled
:label="$t('workspaces')"
:options="localUser?.workspaces"
option-label="name"
v-model="settings.workspace"
@update:model-value="changeWorkspace"
></q-select>
<q-select
dense
:readonly="!user.isPermittedTo('settings', 'write')"
:class="[colorGroup ? 'col-md-4' : 'col-xs-12 col-sm-6 col-md-12', 'q-pa-md']"
filled
:label="$t('databaseName')"
:options="databases"
v-model="settings.databaseName"
></q-input>
@update:model-value="changeDatabase"
>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section>
<q-item-label>{{ scope.opt }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-btn
v-if="scope.index > foundDatabases"
flat
round
size="sm"
color="negative"
icon="cancel"
@click.stop="removeItem(scope.index)"
/>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div v-else class="column items-center q-pa-lg text-center">
<q-icon name="workspaces" size="64px" color="grey-5" />
<div class="text-h6 q-mt-md text-grey-8">
{{ $t('noWorkspaceFound') }}
</div>
</div>
</q-card>
<q-card class="q-ma-lg">
@@ -129,32 +171,81 @@
</div>
</q-card>
</div>
<DialogFrame :width="300" :header-title="$t('addNewDatabase')" ref="addDatabaseRef">
<q-input
class="q-ma-md"
autofocus
filled
:label="$t('databaseName')"
v-model:model-value="newDatabase"
@keyup.enter.stop.prevent="addNewDatabase"
/>
<div class="row justify-center">
<q-btn class="q-a-md" color="primary" :label="$t('save')" @click="addNewDatabase"></q-btn>
</div>
</DialogFrame>
</template>
<script setup lang="ts">
import { logo, appName, databaseName } from 'src/vueLib/models/settings';
import { reactive, ref, watch } from 'vue';
import { logo, appName, databaseName, workspace } from 'src/vueLib/models/settings';
import { onMounted, reactive, ref, watch } from 'vue';
import { appApi } from 'src/boot/axios';
import { useNotify } from 'src/vueLib/general/useNotify';
import { type Settings } from 'src/vueLib/models/settings';
import type { Settings } from 'src/vueLib/models/settings';
import { useUserStore } from 'src/vueLib/login/userStore';
import { setLocalSettings } from 'src/localstorage/localStorage';
import SiteTitle from 'src/vueLib/general/SiteTitle.vue';
import type { User } from 'src/vueLib/models/user';
import { i18n } from 'src/boot/lang';
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
const { NotifyResponse } = useNotify();
const colorGroup = ref(false);
const user = useUserStore();
const addDatabaseRef = ref();
const newDatabase = ref<string>('');
const foundDatabases = ref<number>(0);
const localUser = ref<User>();
const databases = ref<string[]>([]);
const settings = reactive<Settings>({
appName: appName.value,
icon: logo.value,
databaseName: databaseName.value,
workspace: { name: '', description: '', uuid: workspace.value },
primaryColor: document.documentElement.style.getPropertyValue('--q-primary'),
primaryColorText: document.documentElement.style.getPropertyValue('--q-primary-text'),
secondaryColor: document.documentElement.style.getPropertyValue('--q-secondary'),
secondaryColorText: document.documentElement.style.getPropertyValue('--q-secondary-text'),
});
onMounted(async () => {
await appApi
.get('users?id=' + user.user?.id)
.then((resp) => {
if (!resp.data) return;
localUser.value = resp.data[0];
settings.workspace = localUser.value?.settings?.workspace || null;
if (settings.workspace) {
appApi
.post('workspaces/data', settings.workspace)
.then((resp) => {
if (!resp.data) {
settings.databaseName = '';
return;
}
databases.value = [i18n.global.t('addNewDatabase')];
databases.value.push(...resp.data.data);
foundDatabases.value = resp.data.data.length;
})
.catch((err) => NotifyResponse(err, 'error'));
} else {
settings.databaseName = '';
}
})
.catch((err) => NotifyResponse(err, 'error'));
});
watch(settings, (newSettings) => {
logo.value = newSettings.icon;
appName.value = newSettings.appName;
@@ -172,6 +263,40 @@ function resetColors() {
settings.secondaryColorText = '#ffffff';
}
function changeWorkspace() {
appApi
.post('workspaces/data', settings.workspace)
.then((resp) => {
if (resp.data) {
databases.value = [i18n.global.t('addNewDatabase')];
databases.value.push(...resp.data.data);
foundDatabases.value = resp.data.data.length;
}
})
.catch((err) => NotifyResponse(err, 'error'));
}
function changeDatabase() {
if (databases.value.indexOf(settings.databaseName) === 0) {
addDatabaseRef.value?.open();
}
}
function removeItem(index: number) {
if (index > 0) {
databases.value.splice(index, 1);
}
}
function addNewDatabase() {
if (!newDatabase.value.includes('.db')) {
NotifyResponse(i18n.global.t('fileNeedsToEndWith') + ' .db', 'error');
return;
}
databases.value.push(newDatabase.value);
settings.databaseName = newDatabase.value;
addDatabaseRef.value.close();
}
function save() {
document.documentElement.style.setProperty('--q-primary', settings.primaryColor);
document.documentElement.style.setProperty('--q-primary-text', settings.primaryColorText);
@@ -179,16 +304,25 @@ function save() {
document.documentElement.style.setProperty('--q-secondary-text', settings.secondaryColorText);
appName.value = settings.appName;
logo.value = settings.icon;
if (localUser.value?.settings) {
localUser.value.settings = settings;
}
setLocalSettings(settings);
const tempuser = user.user;
if (tempuser) {
tempuser.settings = settings;
}
appApi
.post('users/update', tempuser)
.then((resp) => NotifyResponse(resp.data.message))
.post('users/update', localUser.value)
.then(() =>
NotifyResponse(
i18n.global.t('user') +
' ' +
localUser.value?.user +
' ' +
i18n.global.t('settings') +
' ' +
i18n.global.t('saved'),
),
)
.catch((err) => NotifyResponse(err, 'error'));
}
</script>

View File

@@ -52,7 +52,7 @@ import { appApi } from 'src/boot/axios';
import { i18n } from 'src/boot/lang';
import SiteTitle from 'src/vueLib/general/SiteTitle.vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { databaseName } from 'src/vueLib/models/settings';
import { databaseName, workspace } from 'src/vueLib/models/settings';
import { onMounted, ref } from 'vue';
const stats = ref();
@@ -71,8 +71,10 @@ const amounts = ref<{
const { NotifyResponse } = useNotify();
onMounted(async () => {
let path = databaseName.value;
if (workspace.value !== '') path = workspace.value + '/' + path;
stats.value = await appApi
.post('/stats', { database: databaseName.value })
.post('/stats', { database: path })
.then((resp) => {
if ((resp.data.databaseSize as number) >= 1000000000) {
return (resp.data.data.databaseSize / 1000000000).toFixed(2) + ' GB';

View File

@@ -11,8 +11,15 @@
align="justify"
narrow-indicator
>
<q-tab name="users" no-caps :label="$t('users')" />
<q-tab name="roles" no-caps :label="$t('roles')" />
<q-tab name="users" icon="people" no-caps :label="$t('users')" />
<q-tab name="roles" icon="rule" no-caps :label="$t('roles')" />
<q-tab
v-if="user?.user?.role.role.includes('admin')"
name="workspaces"
icon="workspaces"
no-caps
:label="$t('workspaces')"
/>
</q-tabs>
<q-separator />
@@ -23,16 +30,24 @@
<q-tab-panel name="roles" style="padding: 0px">
<RoleTable />
</q-tab-panel>
<q-tab-panel name="workspaces" style="padding: 0px">
<WorkspaceTable />
</q-tab-panel>
</q-tab-panels>
</q-card>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onMounted, ref } from 'vue';
import UserTable from 'src/vueLib/tables/users/UserTable.vue';
import RoleTable from 'src/vueLib/tables/roles/RoleTable.vue';
import SiteTitle from 'src/vueLib/general/SiteTitle.vue';
import { useUserStore } from 'src/vueLib/login/userStore';
import type { UserState } from 'src/vueLib/models/user';
import WorkspaceTable from 'src/vueLib/tables/workspaces/WorkspaceTable.vue';
const tab = ref('users');
const user = ref<UserState>();
onMounted(() => (user.value = useUserStore()));
</script>

View File

@@ -0,0 +1,14 @@
import { appApi } from 'src/boot/axios';
import { databaseName, workspace } from '../models/settings';
export async function openDatabase() {
let path = databaseName.value;
if (workspace.value !== '') {
path = workspace.value + '/' + path;
}
return appApi.post('database/open', {
dbPath: path,
create: true,
});
}

View File

@@ -3,7 +3,7 @@
<q-btn dense flat round icon="person" :color="currentUser ? 'green' : ''">
<q-menu ref="refLoginMenu">
<q-list style="min-width: 120px">
<q-item v-if="user.user" class="text-primary">{{ currentUser?.username }}</q-item>
<q-item v-if="user.user" class="text-primary">{{ currentUser?.user }}</q-item>
<q-item v-if="showLogin" clickable v-close-popup @click="openLogin">
<q-item-section class="text-primary">{{ loginText }}</q-item-section>
</q-item>
@@ -69,7 +69,7 @@ const darkMode = computed(() => {
return 'dark_mode';
});
const showLogin = computed(
() => (route.path !== '/' && route.path !== '/login') || currentUser.value?.username === '',
() => (route.path !== '/' && route.path !== '/login') || currentUser.value?.user === '',
);
const autorized = computed(() => !!user.isAuthorizedAs(['admin']));

View File

@@ -35,7 +35,7 @@ export function useLogin() {
await userStore
.setUser({
id: resp.data.id,
username: resp.data.user,
user: resp.data.user,
role: { role: resp.data.role, permissions: [] },
})
.catch((err) => NotifyResponse(err, 'error'));
@@ -69,7 +69,7 @@ export function useLogin() {
userStore
.setUser({
id: resp.data.id,
username: resp.data.user,
user: resp.data.user,
role: { role: resp.data.role, permissions: [] },
})
.catch((err) => NotifyResponse(err, 'error'));

View File

@@ -25,8 +25,17 @@ export const useUserStore = defineStore('user', {
};
},
isPermittedTo: (state: UserState) => {
return (name: string, type: 'read' | 'write' | 'delete' | 'import' | 'export'): boolean => {
return (
name: string,
type: 'read' | 'write' | 'delete' | 'import' | 'export',
compareRole?: Role,
): boolean => {
const permission = state.user?.permissions?.find((r: Permission) => r.name === name);
if (compareRole && permission) {
const rolePermission = compareRole.permissions?.find((r: Permission) => r.name === name);
if (rolePermission && rolePermission?.permission > permission?.permission) return false;
}
switch (type) {
case 'read':
return permission?.permission ? (permission.permission & (1 << 0)) === 1 : false;
@@ -62,7 +71,7 @@ export const useUserStore = defineStore('user', {
if (!this.user) return;
if ($q) {
$q?.notify({
message: "user '" + this.user?.username + "' logged out",
message: "user '" + this.user?.user + "' logged out",
color: 'orange',
position: 'top',
icon: 'warning',
@@ -80,7 +89,7 @@ export const useUserStore = defineStore('user', {
],
});
} else {
console.error("user '" + this.user?.username + "' logged out");
console.error("user '" + this.user?.user + "' logged out");
}
this.user = null;
@@ -108,9 +117,12 @@ export const useUserStore = defineStore('user', {
],
});
} else {
console.error("user '" + this.user?.username + "' logged out");
console.error("user '" + this.user?.user + "' logged out");
}
});
},
isAdmin() {
return this.user?.role.role.includes('admin');
},
},
});

View File

@@ -1,13 +1,16 @@
import { ref } from 'vue';
import type { Workspace } from './workspaces';
export const logo = ref('');
export const appName = ref('Attendance Records');
export const databaseName = ref('members.dba');
export const logo = ref<string>('');
export const appName = ref<string>('Attendance Records');
export const databaseName = ref<string>('members.dba');
export const workspace = ref<string>('');
export type Settings = {
appName: string;
icon: string;
databaseName: string;
workspace: Workspace | null;
primaryColor: string;
primaryColorText: string;
secondaryColor: string;
@@ -19,6 +22,7 @@ export function DefaultSettings(): Settings {
appName: 'Attendance Records',
icon: '',
databaseName: 'members.dba',
workspace: { name: '', description: '', uuid: workspace.value },
primaryColor: document.documentElement.style.getPropertyValue('--q-primary-text'),
primaryColorText: document.documentElement.style.getPropertyValue('--q-primary'),
secondaryColor: document.documentElement.style.getPropertyValue('--q-secondary'),

View File

@@ -4,11 +4,12 @@ import type { Settings } from './settings';
export interface User {
id: number;
username: string;
user: string;
role: Role;
permissions?: Permissions;
settings?: Settings;
newDatabase?: boolean;
workspaces?: string[];
}
export interface UserState {

View File

@@ -1,15 +1,18 @@
import type { Role } from './roles';
import type { Settings } from './settings';
import type { Workspace } from './workspaces';
export interface User {
id?: number;
user: string;
email: string;
role?: Role;
roleId?: number;
expiration?: string;
password?: string;
newPassword?: string;
settings?: Settings;
workspaces?: Workspace[];
}
export type Users = User[];

View File

@@ -0,0 +1,8 @@
export interface Workspace {
id?: number;
name: string;
uuid?: string;
description: string;
}
export type Workspaces = Workspace[];

View File

@@ -123,13 +123,13 @@ import EditAllDialog from 'src/components/EventEditAllDialog.vue';
import OkDialog from 'src/components/dialog/OkDialog.vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { useEventTable } from './EventsTable';
import { databaseName } from 'src/vueLib/models/settings';
import { useUserStore } from 'src/vueLib/login/userStore';
import AttendeesTableDialog from '../attendees/AttendeesTableDialog.vue';
import type { Members } from 'src/vueLib/models/member';
import { i18n } from 'src/boot/lang';
import SearchableInput from '../components/SearchableInput.vue';
import TopButtonGroup from '../components/TopButtonGroup.vue';
import { openDatabase } from 'src/vueLib/components/DatabaseCall';
export interface EventDialog {
getSelected: () => Events;
@@ -150,11 +150,10 @@ const user = useUserStore();
const { Events, pagination, loading, columns, updateEvents } = useEventTable();
//load on mounting page
onMounted(() => {
onMounted(async () => {
loading.value = true;
appApi
.post('database/open', { dbPath: databaseName.value, create: true })
await openDatabase()
.then(() => {
updateEvents();
})

View File

@@ -116,7 +116,6 @@ import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
import OkDialog from 'src/components/dialog/OkDialog.vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { useGroupTable } from './GroupTable';
import { databaseName } from 'src/vueLib/models/settings';
import { useUserStore } from 'src/vueLib/login/userStore';
import { i18n } from 'src/boot/lang';
import type { Group, Groups } from 'src/vueLib/models/group';
@@ -124,6 +123,7 @@ import SearchableInput from '../components/SearchableInput.vue';
import TopButtonGroup from '../components/TopButtonGroup.vue';
import { getAllMembers } from '../members/MembersTable';
import type { Members } from 'src/vueLib/models/member';
import { openDatabase } from 'src/vueLib/components/DatabaseCall';
const { NotifyResponse } = useNotify();
const groupDialog = ref();
@@ -147,8 +147,7 @@ onMounted(async () => {
members.value = await getAllMembers();
appApi
.post('database/open', { dbPath: databaseName.value, create: true })
await openDatabase()
.then(() => {
updateGroups().catch((err) => {
NotifyResponse(err, 'error');

View File

@@ -206,7 +206,6 @@ import { useNotify } from 'src/vueLib/general/useNotify';
import { useMemberTable } from './MembersTable';
import UploadDialog from 'src/components/UploadDialog.vue';
import AddToEvent from 'src/components/AddToEvent.vue';
import { databaseName } from 'src/vueLib/models/settings';
import { useUserStore } from 'src/vueLib/login/userStore';
import { i18n } from 'src/boot/lang';
import type { Responsible } from 'src/vueLib/models/responsible';
@@ -216,6 +215,7 @@ import FilterSelect from '../components/FilterSelect.vue';
import TopButtonGroup from '../components/TopButtonGroup.vue';
import { getLocalPageDefaults, setLocalPageDefaults } from 'src/localstorage/localStorage';
import type { PageDefault } from 'src/vueLib/models/pageDefaults';
import { openDatabase } from 'src/vueLib/components/DatabaseCall';
const inProps = defineProps({
addAttendees: { type: Boolean },
@@ -263,7 +263,7 @@ const {
} = useMemberTable();
//load on mounting page
onMounted(() => {
onMounted(async () => {
page.value = 'members';
if (inProps.addAttendees || inProps.addResponsible) {
selectOption.value = true;
@@ -295,8 +295,7 @@ onMounted(() => {
// set custom filter
setNewFilter(selectedColumnFilter.value, ...selectedColumnOptions.value);
appApi
.post('database/open', { dbPath: databaseName.value, create: true })
await openDatabase()
.then(() => {
updateTable().catch((err) => NotifyResponse(err, 'error'));
})

View File

@@ -108,12 +108,12 @@ import type { Members } from 'src/vueLib/models/member';
import OkDialog from 'src/components/dialog/OkDialog.vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { useResponsibleTable } from './ResponsibleTable';
import { databaseName } from 'src/vueLib/models/settings';
import { useUserStore } from 'src/vueLib/login/userStore';
import { i18n } from 'src/boot/lang';
import type { Responsible, Responsibles } from 'src/vueLib/models/responsible';
import SearchableInput from '../components/SearchableInput.vue';
import TopButtonGroup from '../components/TopButtonGroup.vue';
import { openDatabase } from 'src/vueLib/components/DatabaseCall';
const { NotifyResponse } = useNotify();
const responsibleDialog = ref();
@@ -129,11 +129,10 @@ const { responsibleMember, pagination, loading, columns, updateResponsibles } =
useResponsibleTable();
//load on mounting page
onMounted(() => {
onMounted(async () => {
loading.value = true;
appApi
.post('database/open', { dbPath: databaseName.value, create: true })
await openDatabase()
.then(() => {
updateResponsibles().catch((err) => {
NotifyResponse(err, 'error');

View File

@@ -68,7 +68,11 @@ export function useRoleTable() {
.get('/login/me')
.then((resp) => {
userStore
.setUser({ id: resp.data.id, username: resp.data.username, role: resp.data.role })
.setUser({
id: resp.data.id,
user: resp.data.user,
role: { role: resp.data.role, permissions: [] },
})
.catch((err) => NotifyResponse(err, 'error'));
login.refresh().catch((err) => NotifyResponse(err, 'error'));
})

View File

@@ -23,17 +23,11 @@
>
<template v-slot:top-left>
<q-btn-group push flat style="color: grey">
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
dense
flat
icon="add"
@click="openAllValueDialog(null)"
>
<q-btn v-if="writePermisssion" dense flat icon="add" @click="openAllValueDialog(null)">
<q-tooltip>{{ $t('addNewRole') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
v-if="writePermisssion"
dense
flat
style="color: grey"
@@ -72,13 +66,9 @@
<q-td
:props="props"
:disable="!autorized(props.row)"
:style="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
? 'cursor: pointer'
: ''
"
:style="autorized(props.row) && writePermisssion ? 'cursor: pointer' : ''"
@click="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
autorized(props.row) && writePermisssion
? openSingleValueDialog(props.col.label, props.col.name, props.row)
: ''
"
@@ -89,19 +79,18 @@
<template v-slot:body-cell-permissions="props">
<q-td :props="props">
<q-btn
:disable="!autorized(props.row) || !user.isPermittedTo('userSettings', 'write')"
:disable="
!autorized(props.row) || !writePermisssion || user.user?.role.role === props.row.role
"
flat
dense
icon="rule"
:color="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
autorized(props.row) && writePermisssion && user.user?.role.role !== props.row.role
? 'secondary'
: 'grey'
"
@click="
user.isPermittedTo('userSettings', 'write') &&
openAllValueDialog(props.row, 'permissions')
"
@click="writePermisssion && openAllValueDialog(props.row, 'permissions')"
>
<q-tooltip> {{ $t('permissions') }} </q-tooltip>
</q-btn>
@@ -145,7 +134,7 @@
<script setup lang="ts">
import { appApi } from 'src/boot/axios';
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed } from 'vue';
import type { Roles, Role } from 'src/vueLib/models/roles';
import EditOneDialog from 'src/components/EditOneDialog.vue';
import EditAllDialog from 'src/components/RoleEditAllDialog.vue';
@@ -158,6 +147,8 @@ import { useUserStore } from 'src/vueLib/login/userStore';
import SearchableInput from '../components/SearchableInput.vue';
const { NotifyResponse } = useNotify();
const { roles, pagination, loading, columns, updateRoles } = useRoleTable();
const editOneDialog = ref();
const editAllDialog = ref();
const okDialog = ref();
@@ -169,7 +160,7 @@ const currentUser = ref();
const filter = ref('');
const user = useUserStore();
const { roles, pagination, loading, columns, updateRoles } = useRoleTable();
const writePermisssion = computed(() => user.isPermittedTo('userSettings', 'write'));
//load on mounting page
onMounted(() => {
@@ -234,10 +225,6 @@ function removeRole(...removeRoles: Roles) {
</script>
<style>
.blink-yellow {
animation: blink-yellow 1.5s step-start 6 !important;
}
.bigger-table-text .q-table__middle td {
font-size: 14px;
}

View File

@@ -43,6 +43,14 @@ export function useUserTable() {
sortable: true,
style: 'width: 120px; max-width: 120px;',
},
{
name: 'workspaces',
align: 'left' as const,
label: i18n.global.t('workspaces'),
field: 'workspaces',
sortable: true,
style: 'width: 120px; max-width: 120px;',
},
{
name: 'expiration',
align: 'left' as const,
@@ -59,9 +67,9 @@ export function useUserTable() {
const loading = ref(false);
//updates user list from database
function updateUsers() {
async function updateUsers() {
loading.value = true;
appApi
await appApi
.get('users')
.then((resp) => {
if (resp.data === null) {

View File

@@ -9,7 +9,7 @@
:loading-label="$t('loading')"
:rows-per-page-label="$t('recordsPerPage')"
:selected-rows-label="(val) => val + ' ' + $t('recordSelected')"
:rows="users"
:rows="localUsers"
:columns="columns"
row-key="id"
v-model:pagination="pagination"
@@ -23,17 +23,11 @@
>
<template v-slot:top-left>
<q-btn-group push flat style="color: grey">
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
dense
flat
icon="add"
@click="openAllValueDialog(null)"
>
<q-btn v-if="writePermission" dense flat icon="add" @click="openAllValueDialog(null)">
<q-tooltip>{{ $t('addNewUser') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
v-if="writePermission"
dense
flat
style="color: grey"
@@ -43,7 +37,9 @@
<q-tooltip>{{ $t('selectUserOptions') }}</q-tooltip>
</q-btn>
</q-btn-group>
<div v-if="selectOption && selected.length > 0">
<div
v-if="selectOption && selected.length > 0 && user.isPermittedTo('userSettings', 'delete')"
>
<q-btn flat dense icon="more_vert" @click="openSubmenu = true" />
<q-menu v-if="openSubmenu" anchor="bottom middle" self="top middle">
<q-item
@@ -71,13 +67,9 @@
<template v-slot:body-cell="props">
<q-td
:props="props"
:style="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
? 'cursor: pointer'
: ''
"
:style="autorized(props.row) && writePermission ? 'cursor: pointer' : ''"
@click="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
autorized(props.row) && writePermission
? openSingleValueDialog(props.col.label, props.col.name, props.row)
: ''
"
@@ -101,7 +93,11 @@
<template v-slot:body-cell-role="props">
<q-td :props="props">
<q-select
:readonly="!user.isPermittedTo('userSettings', 'write') || !autorized(props.row)"
:readonly="
user.user?.id === props.row.id ||
!user.isPermittedTo('userSettings', 'write', props.row.role) ||
!autorized(props.row)
"
dense
v-model="props.row.role"
:options="localRoles"
@@ -110,16 +106,25 @@
></q-select>
</q-td>
</template>
<template v-slot:body-cell-workspaces="props">
<q-td :props="props">
<q-select
:readonly="props.row.id === user.user?.id || !autorized(props.row) || !writePermission"
dense
v-model="props.row.workspaces"
:options="localWorkspaces"
option-label="name"
multiple
@update:model-value="updateUser(props.row)"
></q-select>
</q-td>
</template>
<template v-slot:body-cell-expiration="props">
<q-td
:props="props"
:style="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
? 'cursor: pointer'
: ''
"
:style="autorized(props.row) && writePermission ? 'cursor: pointer' : ''"
@click="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
autorized(props.row) && writePermission
? openSingleValueDialog(props.col.label, props.col.name, props.row)
: ''
"
@@ -177,31 +182,73 @@ import { i18n } from 'src/boot/lang';
import { useUserStore } from 'src/vueLib/login/userStore';
import ChangePassword from 'src/vueLib/login/ChangePassword.vue';
import SearchableInput from '../components/SearchableInput.vue';
import { useWorkspaceTable } from '../workspaces/WorkspaceTable';
const { NotifyResponse } = useNotify();
const { users, pagination, loading, columns, updateUsers } = useUserTable();
const { updateRoles } = useRoleTable();
const { updateWorkspaces } = useWorkspaceTable();
const user = useUserStore();
const editOneDialog = ref();
const editAllDialog = ref();
const okDialog = ref();
const deleteText = ref('');
const localRoles = computed(() => {
return roles.value.map((role) => role.role);
const localUsers = computed(() => {
return users.value.filter((u) => {
if (user.isAdmin() || user.user?.id === u.id) return user;
const roleP = u.role?.permissions.find((p) => p.name === 'userSettings')?.permission;
const userP = user.user?.permissions?.find((p) => p.name === 'userSettings')?.permission;
const notInWorkspace = u.workspaces?.some((w) => currentUser.value.workspaces.includes(w.name));
if (
roleP === undefined ||
userP === undefined ||
(!notInWorkspace && u.workspaces?.length !== 0)
)
return;
if (userP > roleP) return user;
});
});
const localRoles = computed(() => {
return roles.value.filter((role) => {
const roleP = role.permissions.find((p) => p.name === 'userSettings')?.permission;
const userP = user.user?.permissions?.find((p) => p.name === 'userSettings')?.permission;
if (roleP === undefined || userP === undefined) return;
if (userP < roleP) return;
if (user.isAdmin() || !role.role.includes('admin')) return role;
});
});
const localWorkspaces = computed(() => {
return users.value.filter((u) => u.id === user.user?.id)[0]?.workspaces;
});
const selectOption = ref(false);
const selected = ref<Users>([]);
const openSubmenu = ref(false);
const filter = ref('');
const currentUser = ref();
const { users, pagination, loading, columns, updateUsers } = useUserTable();
const { updateRoles } = useRoleTable();
const user = useUserStore();
const changePwdDialog = ref();
const writePermission = computed(() => user.isPermittedTo('userSettings', 'write'));
//load on mounting page
onMounted(() => {
onMounted(async () => {
loading.value = true;
currentUser.value = user.user;
updateUsers();
updateRoles().catch((err) => NotifyResponse(err, 'error'));
await updateUsers().finally(() => {
const targetUser = users.value.find((u) => u.id === currentUser.value.id);
if (targetUser && targetUser.workspaces) {
currentUser.value.workspaces = targetUser.workspaces.map((ws) => ws.name);
}
});
await updateRoles().catch((err) => NotifyResponse(err, 'error'));
await updateWorkspaces().catch((err) => NotifyResponse(err, 'error'));
// get workspaces of current user
});
//check authorization
@@ -261,7 +308,7 @@ function removeUser(...removeUsers: Users) {
appApi
.post('users/delete?id=' + currentUser.value.id, { ids: userIds })
.then(() => {
updateUsers();
updateUsers().catch((err) => NotifyResponse(err, 'error'));
selected.value = [];
})
.catch((err) => NotifyResponse(err, 'error'))
@@ -272,6 +319,10 @@ function removeUser(...removeUsers: Users) {
// update role select
function updateUser(user: User) {
if (user.role?.id) {
user.roleId = user.role?.id;
}
appApi
.post('/users/update', user)
.then(() => NotifyResponse(i18n.global.t('userUpdated')))

View File

@@ -0,0 +1,78 @@
import { appApi } from 'src/boot/axios';
import { ref, computed } from 'vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { i18n } from 'boot/lang';
import type { Workspaces } from 'src/vueLib/models/workspaces';
// import { useUserStore } from 'src/vueLib/login/userStore';
// import { useLogin } from 'src/vueLib/login/useLogin';
export const workspaces = ref<Workspaces>([]);
export function useWorkspaceTable() {
const pagination = ref({
sortBy: 'name',
descending: false,
page: 1,
rowsPerPage: 20,
});
const columns = computed(() => [
{
name: 'name',
align: 'left' as const,
label: i18n.global.t('name'),
field: 'name',
sortable: true,
},
{
name: 'description',
align: 'left' as const,
label: i18n.global.t('description'),
field: 'description',
style: 'width: 120px; max-width: 120px;',
},
{ name: 'option', align: 'center' as const, label: '', field: 'option', icon: 'option' },
]);
const { NotifyResponse } = useNotify();
const loading = ref(false);
// const userStore = useUserStore();
// const login = useLogin();
//updates user list from database
async function updateWorkspaces() {
loading.value = true;
await appApi
.get('workspaces?id=0')
.then((resp) => {
if (resp.data === null) {
workspaces.value = [];
return;
}
workspaces.value = resp.data as Workspaces;
if (workspaces.value === null) {
workspaces.value = [];
return;
}
})
.catch((err) => {
NotifyResponse(err, 'error');
})
.finally(() => {
loading.value = false;
});
await appApi
.post('users/update', { id: 1, workspaces: workspaces.value })
.catch((err) => NotifyResponse(err, 'error'));
}
return {
workspaces,
pagination,
columns,
loading,
updateWorkspaces,
};
}

View File

@@ -0,0 +1,226 @@
<template>
<q-table
flat
bordered
ref="tableRef"
title="Workspaces"
title-class="text-bold text-blue-9"
:no-data-label="$t('noDataAvailable')"
:loading-label="$t('loading')"
:rows-per-page-label="$t('recordsPerPage')"
:selected-rows-label="(val) => val + ' ' + $t('recordSelected')"
:rows="workspaces"
:columns="columns"
row-key="id"
v-model:pagination="pagination"
:loading="loading"
:filter="filter"
:selection="selectOption ? 'multiple' : 'none'"
v-model:selected="selected"
binary-state-sort
dense
class="bigger-table-text"
>
<template v-slot:top-left>
<q-btn-group push flat style="color: grey">
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
dense
flat
icon="add"
@click="openAllValueDialog(null)"
>
<q-tooltip>{{ $t('addNewWorkspace') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
dense
flat
style="color: grey"
:icon="selectOption ? 'check_box' : 'check_box_outline_blank'"
@click="selectOption = !selectOption"
>
<q-tooltip>{{ $t('selectWorkspaceOptions') }}</q-tooltip>
</q-btn>
</q-btn-group>
<div v-if="selectOption && selected.length > 0">
<q-btn flat dense icon="more_vert" @click="openSubmenu = true" />
<q-menu v-if="openSubmenu" anchor="bottom middle" self="top middle">
<q-item
clickable
v-close-popup
@click="openRemoveDialog(...selected)"
class="text-negative"
>{{ $t('delete') }}</q-item
>
</q-menu>
</div>
<div v-if="selectOption && selected.length > 0" class="q-ml-md text-weight-bold">
{{ $t('selected') }}: {{ selected.length }}
</div>
</template>
<!-- top right of table-->
<template v-slot:top-right>
<SearchableInput v-model="filter" :placeholder="$t('search')" />
</template>
<!-- table body content-->
<template v-slot:body-cell="props">
<q-td
:props="props"
:style="user.isPermittedTo('userSettings', 'write') ? 'cursor: pointer' : ''"
@click="
user.isPermittedTo('userSettings', 'write')
? openSingleValueDialog(props.col.label, props.col.name, props.row)
: ''
"
>
{{ props.value }}
</q-td>
</template>
<template v-slot:body-cell-option="props">
<q-td :props="props">
<q-btn
v-if="user.isPermittedTo('userSettings', 'delete')"
flat
dense
icon="delete"
color="negative"
@click="openRemoveDialog(props.row)"
>
<q-tooltip> {{ $t('delete') }} </q-tooltip>
</q-btn>
</q-td>
</template>
</q-table>
<EditOneDialog
ref="editOneDialog"
endpoint="workspaces/update"
query-id
v-on:update="updateWorkspaces"
></EditOneDialog>
<EditAllDialog ref="editAllDialog" v-on:update="updateWorkspaces"></EditAllDialog>
<OkDialog
ref="okDialog"
:dialog-label="$t('delete')"
:text="$t('doYouWantToDelete') + ' ' + deleteText"
label-color="red"
:button-cancel-label="$t('cancel')"
:button-ok-label="$t('confirm')"
:button-ok-flat="false"
button-ok-color="red"
v-on:update-confirm="(val) => removeWorkspace(...val)"
></OkDialog>
</template>
<script setup lang="ts">
import { appApi } from 'src/boot/axios';
import { ref, onMounted } from 'vue';
import type { Workspaces, Workspace } from 'src/vueLib/models/workspaces';
import EditOneDialog from 'src/components/EditOneDialog.vue';
import EditAllDialog from 'src/components/WorkspaceEditAllDialog.vue';
import OkDialog from 'src/components/dialog/OkDialog.vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { useWorkspaceTable } from './WorkspaceTable';
import { i18n } from 'src/boot/lang';
import { QTable } from 'quasar';
import { useUserStore } from 'src/vueLib/login/userStore';
import SearchableInput from '../components/SearchableInput.vue';
import type { User } from 'src/vueLib/models/user';
import { workspace } from 'src/vueLib/models/settings';
const { NotifyResponse } = useNotify();
const editOneDialog = ref();
const editAllDialog = ref();
const okDialog = ref();
const deleteText = ref('');
const selectOption = ref(false);
const selected = ref<Workspaces>([]);
const openSubmenu = ref(false);
const currentUser = ref<User>();
const filter = ref('');
const user = useUserStore();
const { workspaces, pagination, loading, columns, updateWorkspaces } = useWorkspaceTable();
//load on mounting page
onMounted(() => {
loading.value = true;
if (user.user) {
currentUser.value = user.user;
}
updateWorkspaces().catch((err) => NotifyResponse(err, 'error'));
});
// opens dialog for all workspace values
function openSingleValueDialog(label: string, field: string, workspace: Workspace) {
editOneDialog.value?.open(label, field, workspace);
}
//opens dialog for one value
function openAllValueDialog(workspace: Workspace | null, typ?: 'permissions') {
editAllDialog.value?.open(workspace, typ);
}
//opens remove dialog
function openRemoveDialog(...workspaces: Workspaces) {
if (workspaces.length === 1) {
deleteText.value = "'" + workspaces[0]?.name + "'";
} else {
deleteText.value = String(workspaces.length) + ' ' + i18n.global.t('workspaces');
}
okDialog.value?.open(workspaces);
}
//remove workspace from database
function removeWorkspace(...removeWorkspaces: Workspaces) {
const workspaces: Workspace[] = [];
removeWorkspaces.forEach((workspace: Workspace) => {
if (workspace.id === currentUser.value?.settings?.workspace) {
NotifyResponse(i18n.global.t('notPossibleToDeleteUsedWorkspace'), 'error');
} else if (workspace.id) {
workspaces.push(workspace);
}
});
appApi
.post('workspaces/delete', {
workspaces: workspaces,
})
.then(() => {
const storageWorkspace = localStorage.getItem('workspace');
if (workspaces.some((w) => w.uuid === storageWorkspace)) {
workspace.value = '';
localStorage.removeItem('workspace');
}
updateWorkspaces().catch((err) => NotifyResponse(err, 'error'));
if (workspaces.length === 1) {
NotifyResponse("'" + workspaces[0]?.name + "' " + i18n.global.t('deleted'), 'warning');
} else {
NotifyResponse(i18n.global.t('deleteWorkspaces'), 'warning');
}
selected.value = [];
})
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
loading.value = false;
selectOption.value = false;
});
}
</script>
<style>
.bigger-table-text .q-table__middle td {
font-size: 14px;
}
.bigger-table-text .q-table__top,
.bigger-table-text .q-table__bottom,
.bigger-table-text th {
font-size: 14px;
}
</style>