Compare commits
22 Commits
v1.3.1
...
9e460db854
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e460db854 | ||
|
|
fa58872840 | ||
|
|
ec5893db57 | ||
|
|
21bbfd617e | ||
|
|
d154041df0 | ||
| 41e0b4d060 | |||
| 2f67c02be1 | |||
|
|
15f9026a5f | ||
|
|
e25bac1a1e | ||
|
|
548cd9d622 | ||
|
|
8e3e8f8bc7 | ||
|
|
e686a27bf1 | ||
|
|
ab88acd740 | ||
|
|
6392877dc1 | ||
|
|
b726eb42dc | ||
|
|
8963cba016 | ||
|
|
73901335a3 | ||
|
|
62aed501f3 | ||
|
|
43d81dd27a | ||
|
|
ce654bbb6a | ||
|
|
9b2b1d3ef7 | ||
|
|
f59443ce5a |
@@ -1,10 +1,10 @@
|
||||
# Memer App
|
||||
# Member App
|
||||
|
||||
A full-stack Member Management platform designed for organizations to manage their members, events, excursions, and Impact Team activities efficiently.
|
||||
|
||||
## 🧩 Overview
|
||||
|
||||
**Memer App** provides a unified interface for handling:
|
||||
**Member App** provides a unified interface for handling:
|
||||
- Member registration and profiles
|
||||
- Event creation, attendance tracking, and reporting
|
||||
- Excursion & Impact Team management
|
||||
|
||||
BIN
backend/gagag.dbaa
Normal file
BIN
backend/gagag.dbaa
Normal file
Binary file not shown.
@@ -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.51
|
||||
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
|
||||
|
||||
@@ -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.51 h1:kTPwN+0Zw/Uyfo6el1jl5ORzIrjQdC8PUlczoN9mBS4=
|
||||
gitea.tecamino.com/paadi/access-handler v1.0.51/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=
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lightcontrol",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.1",
|
||||
"description": "A Tecamino App",
|
||||
"productName": "Attendence Records",
|
||||
"author": "A. Zuercher",
|
||||
|
||||
@@ -170,3 +170,38 @@ week: Wuche
|
||||
month: Monat
|
||||
year: Jahr
|
||||
appName: Applikationsname
|
||||
calendar:
|
||||
days:
|
||||
- 'Suntig'
|
||||
- 'Mäntig'
|
||||
- 'Zistig'
|
||||
- 'Mittwuch'
|
||||
- 'Donstig'
|
||||
- 'Fritig'
|
||||
- 'Samstig'
|
||||
daysShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']
|
||||
months:
|
||||
- 'Januar'
|
||||
- 'Februar'
|
||||
- 'März'
|
||||
- 'April'
|
||||
- 'Mai'
|
||||
- 'Juni'
|
||||
- 'Juli'
|
||||
- 'Ougust'
|
||||
- 'Septämber'
|
||||
- 'Oktober'
|
||||
- 'Novämber'
|
||||
- 'Dezämber'
|
||||
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -170,3 +170,38 @@ week: Woche
|
||||
month: Monat
|
||||
year: Jahr
|
||||
appName: Applikationsname
|
||||
calendar:
|
||||
days:
|
||||
- 'Sonntag'
|
||||
- 'Montag'
|
||||
- 'Dienstag'
|
||||
- 'Mittwoch'
|
||||
- 'Donnerstag'
|
||||
- 'Freitag'
|
||||
- 'Samstag'
|
||||
daysShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']
|
||||
months:
|
||||
- 'Januar'
|
||||
- 'Februar'
|
||||
- 'März'
|
||||
- 'April'
|
||||
- 'Mai'
|
||||
- 'Juni'
|
||||
- 'Juli'
|
||||
- 'August'
|
||||
- 'September'
|
||||
- 'Oktober'
|
||||
- 'November'
|
||||
- 'Dezember'
|
||||
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
|
||||
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
|
||||
|
||||
@@ -170,3 +170,38 @@ week: Week
|
||||
month: Month
|
||||
year: Year
|
||||
appName: Applicationname
|
||||
calendar:
|
||||
days:
|
||||
- 'Sunday'
|
||||
- 'Monday'
|
||||
- 'Tuesday'
|
||||
- 'Wednesday'
|
||||
- 'Thursday'
|
||||
- 'Friday'
|
||||
- 'Saturday'
|
||||
daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat']
|
||||
months:
|
||||
- 'January'
|
||||
- 'February'
|
||||
- 'March'
|
||||
- 'April'
|
||||
- 'May'
|
||||
- 'June'
|
||||
- 'July'
|
||||
- 'August'
|
||||
- 'September'
|
||||
- 'October'
|
||||
- 'November'
|
||||
- 'December'
|
||||
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -170,3 +170,38 @@ week: Semana
|
||||
month: Mes
|
||||
year: Año
|
||||
appName: Nombre de la aplicación
|
||||
calendar:
|
||||
days:
|
||||
- 'Domingo'
|
||||
- 'Lunes'
|
||||
- 'Martes'
|
||||
- 'Miércoles'
|
||||
- 'Jueves'
|
||||
- 'Viernes'
|
||||
- 'Sábado'
|
||||
daysShort: ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb']
|
||||
months:
|
||||
- 'Enero'
|
||||
- 'Febrero'
|
||||
- 'Marzo'
|
||||
- 'Abril'
|
||||
- 'Mayo'
|
||||
- 'Junio'
|
||||
- 'Julio'
|
||||
- 'Agosto'
|
||||
- 'Septiembre'
|
||||
- 'Octubre'
|
||||
- 'Noviembre'
|
||||
- 'Diciembre'
|
||||
monthsShort: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic']
|
||||
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
|
||||
|
||||
@@ -1,29 +1,35 @@
|
||||
import { boot } from 'quasar/wrappers';
|
||||
import { appApi } from './axios';
|
||||
import { createPinia } from 'pinia';
|
||||
import { useUserStore } from 'src/vueLib/login/userStore';
|
||||
import { useLogin } from 'src/vueLib/login/useLogin';
|
||||
import { Me, openDatabase } from 'src/vueLib/components/DatabaseCall';
|
||||
|
||||
const pinia = createPinia();
|
||||
|
||||
export default boot(async ({ app }) => {
|
||||
app.use(pinia);
|
||||
const useStore = useUserStore();
|
||||
const userStore = useUserStore();
|
||||
const login = useLogin();
|
||||
|
||||
await appApi
|
||||
.get('/login/me')
|
||||
.then((resp) => {
|
||||
useStore
|
||||
.setUser({
|
||||
id: resp.data.id,
|
||||
username: resp.data.username,
|
||||
role: { role: resp.data.role, permissions: [] },
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
login.refresh().catch((err) => console.error(err));
|
||||
const resp = await Me().catch(() =>
|
||||
login.logout().catch((err) => {
|
||||
console.error(err);
|
||||
return;
|
||||
}),
|
||||
);
|
||||
|
||||
if (!resp) return;
|
||||
|
||||
await userStore
|
||||
.setUser({
|
||||
id: resp.data.id,
|
||||
user: resp.data.username,
|
||||
role: { role: resp.data.role, permissions: [] },
|
||||
workspaceId: resp.data.workspaceId,
|
||||
settings: resp.data.settings,
|
||||
})
|
||||
.catch(() => {
|
||||
login.logout().catch((err) => console.error(err));
|
||||
});
|
||||
.catch((err) => console.error(err));
|
||||
login.refresh().catch((err) => console.error(err));
|
||||
|
||||
await openDatabase().catch((err) => console.error(err));
|
||||
});
|
||||
|
||||
@@ -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 { logo, appName } from 'src/vueLib/models/settings';
|
||||
import { Dark } from 'quasar';
|
||||
import { getLocalDarkMode, getLocalSettings } from 'src/localstorage/localStorage';
|
||||
|
||||
@@ -20,7 +20,6 @@ export default boot(({ app, router }) => {
|
||||
if (settings.appName) {
|
||||
appName.value = settings.appName;
|
||||
}
|
||||
databaseName.value = settings.databaseName ?? databaseName.value;
|
||||
|
||||
document.documentElement.style.setProperty('--q-primary', settings.primaryColor ?? '#1976d2');
|
||||
document.documentElement.style.setProperty(
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { boot } from 'quasar/wrappers';
|
||||
import { useUserStore } from 'src/vueLib/login/userStore';
|
||||
import { appApi } from './axios';
|
||||
import { getLocalLastRoute, setLocalLastRoute } from 'src/localstorage/localStorage';
|
||||
import { Me } from 'src/vueLib/components/DatabaseCall';
|
||||
|
||||
export default boot(async ({ router }) => {
|
||||
const userStore = useUserStore();
|
||||
// load user
|
||||
try {
|
||||
const { data } = await appApi.get('/login/me');
|
||||
const data = await Me();
|
||||
userStore.setFirstLogin(data.newDatabase);
|
||||
|
||||
data.role.role = data.role;
|
||||
await userStore.setUser(data);
|
||||
} catch {
|
||||
|
||||
@@ -74,7 +74,9 @@ function open(title: string, members: Members) {
|
||||
appApi
|
||||
.get('events')
|
||||
.then((resp) => {
|
||||
events.value.push(...resp.data.map((e: Event) => (e.name = e.name + ' (' + e.date + ')')));
|
||||
events.value.push(
|
||||
...resp.data.map((e: Event) => ({ ...e, name: e.name + ' (' + e.date + ')' })),
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
NotifyResponse(err, 'error');
|
||||
@@ -109,7 +111,7 @@ async function addAttendees() {
|
||||
});
|
||||
|
||||
await updateAttendees(0);
|
||||
updateEvents();
|
||||
await updateEvents();
|
||||
}
|
||||
|
||||
let resolveNewEvent!: (value: Event) => void;
|
||||
|
||||
@@ -31,16 +31,16 @@
|
||||
</q-tabs>
|
||||
</div>
|
||||
<div class="row">
|
||||
<q-date v-model="dateRange" range flat />
|
||||
<q-date :locale="calendarLanguage" v-model="dateRange" range flat />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, type PropType } from 'vue';
|
||||
import { ref, onMounted, watch, type PropType, computed } from 'vue';
|
||||
import { date } from 'quasar';
|
||||
import { i18n } from 'src/boot/lang';
|
||||
|
||||
import type { QDateLocale } from 'src/vueLib/models/qDateLocale';
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
height: { type: Number, default: 400 },
|
||||
@@ -74,6 +74,20 @@ const weekdayOptions = [
|
||||
{ label: i18n.global.t('SundayShort'), value: 0 },
|
||||
];
|
||||
|
||||
const calendarLanguage = computed(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
const localeData = i18n.global.tm('calendar') as unknown as QDateLocale;
|
||||
return {
|
||||
days: localeData.days,
|
||||
daysShort: localeData.daysShort,
|
||||
months: localeData.months,
|
||||
monthsShort: localeData.monthsShort,
|
||||
firstDayOfWeek: localeData.firstDayOfWeek,
|
||||
format24h: localeData.format24h,
|
||||
pluralDay: localeData.pluralDay,
|
||||
};
|
||||
});
|
||||
|
||||
const onTabChange = (val: 'today' | 'week' | 'month' | 'year') => {
|
||||
if (val) setRange(val);
|
||||
// Optional: Reset tab to empty so user can click the same tab again later
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
:label="$t('responsible')"
|
||||
filled
|
||||
:options="props.responsibles"
|
||||
:option-label="(opt) => opt.firstName + ' ' + opt.lastName"
|
||||
:option-label="(opt) => opt.member.firstName + ' ' + opt.member.lastName"
|
||||
v-model="localMember.responsible"
|
||||
></q-select>
|
||||
<q-input
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
105
src/components/WorkspaceEditAllDialog.vue
Normal file
105
src/components/WorkspaceEditAllDialog.vue
Normal 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>
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Dark } from 'quasar';
|
||||
import { appName, databaseName, type Settings } from 'src/vueLib/models/settings';
|
||||
import { appName, 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);
|
||||
if (settings.icon !== '') localStorage.setItem('icon', settings.icon);
|
||||
if (settings.appName !== '') localStorage.setItem('appName', settings.appName);
|
||||
localStorage.setItem('primaryColor', settings.primaryColor);
|
||||
localStorage.setItem('primaryColorText', settings.primaryColorText);
|
||||
localStorage.setItem('secondaryColor', settings.secondaryColor);
|
||||
@@ -17,12 +16,6 @@ export function getLocalSettings(): Settings {
|
||||
if (name === undefined || name === 'undefined') {
|
||||
name = appName.value;
|
||||
}
|
||||
|
||||
let db = localStorage.getItem('databaseName');
|
||||
if (db === undefined || db === 'undefined') {
|
||||
db = databaseName.value;
|
||||
}
|
||||
|
||||
let iconName = localStorage.getItem('icon');
|
||||
if (iconName === undefined || iconName === 'undefined') {
|
||||
iconName = '';
|
||||
@@ -31,7 +24,6 @@ export function getLocalSettings(): Settings {
|
||||
return <Settings>{
|
||||
icon: iconName,
|
||||
appName: name,
|
||||
databaseName: db,
|
||||
primaryColor: localStorage.getItem('primaryColor'),
|
||||
primaryColorText: localStorage.getItem('primaryColorText'),
|
||||
secondaryColor: localStorage.getItem('secondaryColor'),
|
||||
@@ -42,7 +34,6 @@ export function getLocalSettings(): Settings {
|
||||
export function clearLocalStorage() {
|
||||
localStorage.removeItem('icon');
|
||||
localStorage.removeItem('appName');
|
||||
localStorage.removeItem('databaseName');
|
||||
localStorage.removeItem('primaryColor');
|
||||
localStorage.removeItem('primaryColorText');
|
||||
localStorage.removeItem('secondaryColor');
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,7 +140,7 @@ 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';
|
||||
@@ -150,6 +160,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(() => [
|
||||
{
|
||||
@@ -170,13 +181,6 @@ const columns = computed(() => [
|
||||
|
||||
onMounted(() => {
|
||||
loading.value = true;
|
||||
appApi
|
||||
.post('database/open', { dbPath: databaseName.value })
|
||||
.catch((err) => NotifyResponse(err, 'error'))
|
||||
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
appApi
|
||||
.get('/groups')
|
||||
.then((resp) => (groups.value = resp.data))
|
||||
@@ -268,10 +272,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 +316,8 @@ async function downloadPDF() {
|
||||
.save()
|
||||
.catch((error) => {
|
||||
console.error('PDF Generation failed:', error);
|
||||
});
|
||||
})
|
||||
.finally(() => (printing.value = false));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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="localWorkspace"
|
||||
@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,36 +171,88 @@
|
||||
</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 } 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';
|
||||
import type { Workspace } from 'src/vueLib/models/workspaces';
|
||||
import { openDatabase } from '../vueLib/components/DatabaseCall';
|
||||
|
||||
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 localWorkspace = ref<Workspace>();
|
||||
|
||||
const settings = reactive<Settings>({
|
||||
appName: appName.value,
|
||||
icon: logo.value,
|
||||
databaseName: databaseName.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];
|
||||
localWorkspace.value = localUser.value?.workspaces?.find(
|
||||
(w) => w.id === localUser.value?.workspaceId,
|
||||
);
|
||||
|
||||
if (localUser.value) {
|
||||
settings.databaseName = localUser.value.settings?.databaseName || '';
|
||||
|
||||
appApi
|
||||
.post('workspaces/data', localWorkspace.value)
|
||||
.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'));
|
||||
}
|
||||
})
|
||||
.catch((err) => NotifyResponse(err, 'error'));
|
||||
});
|
||||
|
||||
watch(settings, (newSettings) => {
|
||||
logo.value = newSettings.icon;
|
||||
appName.value = newSettings.appName;
|
||||
databaseName.value = newSettings.databaseName;
|
||||
});
|
||||
|
||||
function resetColors() {
|
||||
@@ -172,23 +266,74 @@ function resetColors() {
|
||||
settings.secondaryColorText = '#ffffff';
|
||||
}
|
||||
|
||||
function save() {
|
||||
function changeWorkspace() {
|
||||
appApi
|
||||
.post('workspaces/data', localWorkspace.value)
|
||||
.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 (settings.databaseName) {
|
||||
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();
|
||||
}
|
||||
|
||||
async function save() {
|
||||
document.documentElement.style.setProperty('--q-primary', settings.primaryColor);
|
||||
document.documentElement.style.setProperty('--q-primary-text', settings.primaryColorText);
|
||||
document.documentElement.style.setProperty('--q-secondary', settings.secondaryColor);
|
||||
document.documentElement.style.setProperty('--q-secondary-text', settings.secondaryColorText);
|
||||
appName.value = settings.appName;
|
||||
logo.value = settings.icon;
|
||||
setLocalSettings(settings);
|
||||
|
||||
const tempuser = user.user;
|
||||
|
||||
if (tempuser) {
|
||||
tempuser.settings = settings;
|
||||
if (localUser.value?.settings) {
|
||||
localUser.value.settings = settings;
|
||||
}
|
||||
appApi
|
||||
.post('users/update', tempuser)
|
||||
.then((resp) => NotifyResponse(resp.data.message))
|
||||
|
||||
setLocalSettings(settings);
|
||||
if (localUser.value) {
|
||||
await user.setUser(localUser.value);
|
||||
localUser.value.workspaceId = localWorkspace.value?.id;
|
||||
}
|
||||
|
||||
await appApi
|
||||
.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'));
|
||||
|
||||
await openDatabase().catch((err) => console.error(err));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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 { useUserStore } from 'src/vueLib/login/userStore';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const stats = ref();
|
||||
@@ -71,8 +71,13 @@ const amounts = ref<{
|
||||
const { NotifyResponse } = useNotify();
|
||||
|
||||
onMounted(async () => {
|
||||
const user = useUserStore().user;
|
||||
const workspaceUuid = useUserStore().getWorkspaceUuid;
|
||||
let path = user?.settings?.databaseName;
|
||||
if (workspaceUuid !== undefined) path = workspaceUuid + '/' + 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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
43
src/vueLib/components/DatabaseCall.ts
Normal file
43
src/vueLib/components/DatabaseCall.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { appApi } from 'src/boot/axios';
|
||||
import { useUserStore } from '../login/userStore';
|
||||
import type { User } from '../models/users';
|
||||
import { useNotify } from '../general/useNotify';
|
||||
|
||||
export async function openDatabase() {
|
||||
const user = useUserStore().user;
|
||||
|
||||
if (!user) return;
|
||||
const tempUser = (await getUser(user?.id)) as User | void;
|
||||
|
||||
if (!tempUser) return;
|
||||
let path = '';
|
||||
|
||||
path = tempUser.settings?.databaseName || '';
|
||||
|
||||
if (tempUser.workspaces) {
|
||||
path = tempUser.workspaces.find((w) => w.id === tempUser.workspaceId)?.uuid || '';
|
||||
path = path + '/' + tempUser.settings?.databaseName;
|
||||
}
|
||||
|
||||
return appApi.post('database/open', {
|
||||
dbPath: path,
|
||||
create: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function getUser(id: number): Promise<User | null> {
|
||||
const { NotifyResponse } = useNotify();
|
||||
return appApi
|
||||
.get(`/users?id=${id}`)
|
||||
.then((resp) => {
|
||||
return resp.data[0];
|
||||
})
|
||||
.catch((err) => {
|
||||
NotifyResponse(err, 'error');
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
export async function Me() {
|
||||
return appApi.get('/login/me');
|
||||
}
|
||||
@@ -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']));
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Settings } from '../models/settings';
|
||||
import { appName, logo } from '../models/settings';
|
||||
import { clearLocalStorage, setLocalSettings } from 'src/localstorage/localStorage';
|
||||
import { routerInstance } from 'src/router';
|
||||
import { Me } from '../components/DatabaseCall';
|
||||
|
||||
const refreshTime = 10000;
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||
@@ -31,12 +32,14 @@ export function useLogin() {
|
||||
setLocalSettings(sets);
|
||||
});
|
||||
|
||||
const resp = await appApi.get('/login/me');
|
||||
const resp = await Me();
|
||||
|
||||
await userStore
|
||||
.setUser({
|
||||
id: resp.data.id,
|
||||
username: resp.data.user,
|
||||
user: resp.data.user,
|
||||
role: { role: resp.data.role, permissions: [] },
|
||||
workspaceId: resp.data.workspace,
|
||||
})
|
||||
.catch((err) => NotifyResponse(err, 'error'));
|
||||
|
||||
@@ -60,31 +63,31 @@ export function useLogin() {
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await appApi
|
||||
.post('login/refresh', {}, { withCredentials: true })
|
||||
.then(() => {
|
||||
appApi
|
||||
.get('/login/me')
|
||||
.then((resp) => {
|
||||
userStore
|
||||
.setUser({
|
||||
id: resp.data.id,
|
||||
username: resp.data.user,
|
||||
role: { role: resp.data.role, permissions: [] },
|
||||
})
|
||||
.catch((err) => NotifyResponse(err, 'error'));
|
||||
if (!intervalId) {
|
||||
startRefreshInterval();
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.catch(() => {});
|
||||
await appApi.post('login/refresh', {}, { withCredentials: true }).catch(() => {
|
||||
userStore.clearUser();
|
||||
return;
|
||||
});
|
||||
|
||||
const resp = await Me();
|
||||
if (!resp) {
|
||||
stopRefreshInterval();
|
||||
return false;
|
||||
}
|
||||
userStore
|
||||
.setUser({
|
||||
id: resp.data.id,
|
||||
user: resp.data.user,
|
||||
role: { role: resp.data.role, permissions: [] },
|
||||
workspaceId: resp.data.workspace,
|
||||
settings: resp.data.settings,
|
||||
})
|
||||
.catch(() => {
|
||||
userStore.clearUser();
|
||||
});
|
||||
stopRefreshInterval();
|
||||
return false;
|
||||
.catch((err) => NotifyResponse(err, 'error'));
|
||||
|
||||
if (!intervalId) {
|
||||
startRefreshInterval();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function startRefreshInterval() {
|
||||
|
||||
@@ -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;
|
||||
@@ -41,6 +50,9 @@ export const useUserStore = defineStore('user', {
|
||||
}
|
||||
};
|
||||
},
|
||||
getWorkspaceUuid: (state: UserState) => {
|
||||
return state.user?.workspaces?.find((w) => w.id === state.user?.workspaceId)?.uuid;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setFirstLogin(b: boolean) {
|
||||
@@ -62,7 +74,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 +92,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 +120,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');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
9
src/vueLib/models/qDateLocale.ts
Normal file
9
src/vueLib/models/qDateLocale.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface QDateLocale {
|
||||
days: string[];
|
||||
daysShort: string[];
|
||||
months: string[];
|
||||
monthsShort: string[];
|
||||
firstDayOfWeek: number;
|
||||
format24h: boolean;
|
||||
pluralDay: string;
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
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 type Settings = {
|
||||
appName: string;
|
||||
icon: string;
|
||||
databaseName: string;
|
||||
databaseName?: string;
|
||||
primaryColor: string;
|
||||
primaryColorText: string;
|
||||
secondaryColor: string;
|
||||
@@ -18,7 +17,6 @@ export function DefaultSettings(): Settings {
|
||||
return {
|
||||
appName: 'Attendance Records',
|
||||
icon: '',
|
||||
databaseName: 'members.dba',
|
||||
primaryColor: document.documentElement.style.getPropertyValue('--q-primary-text'),
|
||||
primaryColorText: document.documentElement.style.getPropertyValue('--q-primary'),
|
||||
secondaryColor: document.documentElement.style.getPropertyValue('--q-secondary'),
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import type { Permissions } from '../checkboxes/permissions';
|
||||
import type { Role } from './roles';
|
||||
import type { Settings } from './settings';
|
||||
import type { Workspaces } from './workspaces';
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
user: string;
|
||||
role: Role;
|
||||
permissions?: Permissions;
|
||||
settings?: Settings;
|
||||
newDatabase?: boolean;
|
||||
workspaceId?: number | undefined;
|
||||
workspaces?: Workspaces;
|
||||
}
|
||||
|
||||
export interface UserState {
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
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;
|
||||
workspaceId?: number;
|
||||
workspaces?: Workspace[];
|
||||
}
|
||||
|
||||
export type Users = User[];
|
||||
|
||||
8
src/vueLib/models/workspaces.ts
Normal file
8
src/vueLib/models/workspaces.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface Workspace {
|
||||
id?: number;
|
||||
name: string;
|
||||
uuid?: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type Workspaces = Workspace[];
|
||||
@@ -63,10 +63,10 @@ export function useEventTable() {
|
||||
const loading = ref(false);
|
||||
|
||||
//updates Event list from database
|
||||
function updateEvents() {
|
||||
async function updateEvents() {
|
||||
loading.value = true;
|
||||
|
||||
appApi
|
||||
await appApi
|
||||
.get('events')
|
||||
.then((resp) => {
|
||||
if (resp.data === null) {
|
||||
|
||||
@@ -116,14 +116,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { appApi } from 'src/boot/axios';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { Event, Events } from 'src/vueLib/models/event';
|
||||
import EditOneDialog from 'src/components/EditOneDialog.vue';
|
||||
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';
|
||||
@@ -149,20 +148,8 @@ const user = useUserStore();
|
||||
|
||||
const { Events, pagination, loading, columns, updateEvents } = useEventTable();
|
||||
|
||||
//load on mounting page
|
||||
onMounted(() => {
|
||||
loading.value = true;
|
||||
|
||||
appApi
|
||||
.post('database/open', { dbPath: databaseName.value, create: true })
|
||||
.then(() => {
|
||||
updateEvents();
|
||||
})
|
||||
.catch((err) => NotifyResponse(err, 'error'))
|
||||
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
onMounted(async () => {
|
||||
await updateEvents();
|
||||
});
|
||||
|
||||
// opens dialog for all Event values
|
||||
@@ -194,17 +181,17 @@ function openAttendees(eventArray: number, attendees: Members | null) {
|
||||
}
|
||||
|
||||
//remove Event from database
|
||||
function removeEvent(...removeEvents: Events) {
|
||||
async function removeEvent(...removeEvents: Events) {
|
||||
const EventIds: number[] = [];
|
||||
|
||||
removeEvents.forEach((Event: Event) => {
|
||||
EventIds.push(Event.id);
|
||||
});
|
||||
|
||||
appApi
|
||||
await appApi
|
||||
.post('events/delete', { ids: EventIds })
|
||||
.then(() => {
|
||||
updateEvents();
|
||||
updateEvents().catch((err) => NotifyResponse(err, 'error'));
|
||||
selected.value = [];
|
||||
})
|
||||
.catch((err) => NotifyResponse(err, 'error'))
|
||||
|
||||
@@ -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';
|
||||
@@ -146,19 +145,8 @@ onMounted(async () => {
|
||||
loading.value = true;
|
||||
|
||||
members.value = await getAllMembers();
|
||||
|
||||
appApi
|
||||
.post('database/open', { dbPath: databaseName.value, create: true })
|
||||
.then(() => {
|
||||
updateGroups().catch((err) => {
|
||||
NotifyResponse(err, 'error');
|
||||
});
|
||||
})
|
||||
.catch((err) => NotifyResponse(err, 'error'))
|
||||
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
await updateGroups();
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
//opens dialog for one value
|
||||
|
||||
@@ -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';
|
||||
@@ -263,7 +262,7 @@ const {
|
||||
} = useMemberTable();
|
||||
|
||||
//load on mounting page
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
page.value = 'members';
|
||||
if (inProps.addAttendees || inProps.addResponsible) {
|
||||
selectOption.value = true;
|
||||
@@ -295,20 +294,17 @@ onMounted(() => {
|
||||
// set custom filter
|
||||
setNewFilter(selectedColumnFilter.value, ...selectedColumnOptions.value);
|
||||
|
||||
appApi
|
||||
.post('database/open', { dbPath: databaseName.value, create: true })
|
||||
.then(() => {
|
||||
updateTable().catch((err) => NotifyResponse(err, 'error'));
|
||||
})
|
||||
.catch((err) => NotifyResponse(err, 'error'))
|
||||
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
await updateMembers(localCompareMembers.value, inProps.addResponsible).catch((err) =>
|
||||
NotifyResponse(err, 'error'),
|
||||
);
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
async function updateTable() {
|
||||
async function updateTable(add?: Members) {
|
||||
localCompareMembers.value = inProps.compareMembers;
|
||||
if (add) {
|
||||
localCompareMembers.value?.push(...add);
|
||||
}
|
||||
await updateMembers(localCompareMembers.value, inProps.addResponsible).catch((err) =>
|
||||
NotifyResponse(err, 'error'),
|
||||
);
|
||||
@@ -452,15 +448,15 @@ async function addMemberTo() {
|
||||
})
|
||||
.catch((err) => {
|
||||
NotifyResponse(err, 'error');
|
||||
})
|
||||
.finally(() => (selected.value = []));
|
||||
});
|
||||
|
||||
if (inProps.addAttendees) {
|
||||
await updateMemberLastVisit(selected.value);
|
||||
} else {
|
||||
await updateTable();
|
||||
await updateTable(selected.value);
|
||||
emit('update-event', filteredMembers.value.length);
|
||||
}
|
||||
selected.value = [];
|
||||
}
|
||||
|
||||
async function updateMemberLastVisit(members: Members) {
|
||||
|
||||
@@ -101,14 +101,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { appApi } from 'src/boot/axios';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
|
||||
import MembersTable from '../members/MembersTable.vue';
|
||||
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';
|
||||
@@ -128,22 +127,8 @@ const user = useUserStore();
|
||||
const { responsibleMember, pagination, loading, columns, updateResponsibles } =
|
||||
useResponsibleTable();
|
||||
|
||||
//load on mounting page
|
||||
onMounted(() => {
|
||||
loading.value = true;
|
||||
|
||||
appApi
|
||||
.post('database/open', { dbPath: databaseName.value, create: true })
|
||||
.then(() => {
|
||||
updateResponsibles().catch((err) => {
|
||||
NotifyResponse(err, 'error');
|
||||
});
|
||||
})
|
||||
.catch((err) => NotifyResponse(err, 'error'))
|
||||
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
onMounted(async () => {
|
||||
await updateResponsibles();
|
||||
});
|
||||
|
||||
//opens dialog for one value
|
||||
|
||||
@@ -5,6 +5,7 @@ import { i18n } from 'boot/lang';
|
||||
import type { Roles } from 'src/vueLib/models/roles';
|
||||
import { useUserStore } from 'src/vueLib/login/userStore';
|
||||
import { useLogin } from 'src/vueLib/login/useLogin';
|
||||
import { Me } from 'src/vueLib/components/DatabaseCall';
|
||||
|
||||
export const roles = ref<Roles>([]);
|
||||
|
||||
@@ -64,17 +65,27 @@ export function useRoleTable() {
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
await appApi
|
||||
.get('/login/me')
|
||||
.then((resp) => {
|
||||
userStore
|
||||
.setUser({ id: resp.data.id, username: resp.data.username, role: resp.data.role })
|
||||
.catch((err) => NotifyResponse(err, 'error'));
|
||||
login.refresh().catch((err) => NotifyResponse(err, 'error'));
|
||||
})
|
||||
.catch(() => {
|
||||
login.logout().catch((err) => NotifyResponse(err, 'error'));
|
||||
|
||||
const resp = await Me().catch(() => {
|
||||
login.logout().catch((err) => {
|
||||
NotifyResponse(err, 'error');
|
||||
return;
|
||||
});
|
||||
});
|
||||
|
||||
if (!resp) return;
|
||||
|
||||
await userStore
|
||||
.setUser({
|
||||
id: resp.data.id,
|
||||
user: resp.data.user,
|
||||
role: { role: resp.data.role, permissions: [] },
|
||||
workspaceId: resp.data.workspaceId,
|
||||
settings: resp.data.settings,
|
||||
})
|
||||
.catch((err) => NotifyResponse(err, 'error'));
|
||||
|
||||
login.refresh().catch((err) => NotifyResponse(err, 'error'));
|
||||
}
|
||||
return {
|
||||
roles,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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')))
|
||||
|
||||
78
src/vueLib/tables/workspaces/WorkspaceTable.ts
Normal file
78
src/vueLib/tables/workspaces/WorkspaceTable.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
223
src/vueLib/tables/workspaces/WorkspaceTable.vue
Normal file
223
src/vueLib/tables/workspaces/WorkspaceTable.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<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';
|
||||
|
||||
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[] = [];
|
||||
|
||||
const user = useUserStore().user;
|
||||
const usedWorkspaceId = user?.workspaces?.find((w) => w.uuid === user.workspaceId)?.id;
|
||||
|
||||
removeWorkspaces.forEach((workspace: Workspace) => {
|
||||
if (workspace.id === usedWorkspaceId) {
|
||||
NotifyResponse(i18n.global.t('notPossibleToDeleteUsedWorkspace'), 'error');
|
||||
} else if (workspace.id) {
|
||||
workspaces.push(workspace);
|
||||
}
|
||||
});
|
||||
|
||||
appApi
|
||||
.post('workspaces/delete', {
|
||||
workspaces: workspaces,
|
||||
})
|
||||
.then(() => {
|
||||
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>
|
||||
Reference in New Issue
Block a user