Added Frontend

This commit is contained in:
2026-01-06 12:09:31 +01:00
250 changed files with 29602 additions and 0 deletions

168
frontend/stores/auth.ts Normal file
View File

@@ -0,0 +1,168 @@
import { defineStore } from "pinia"
import router from "#app/plugins/router";
import {Preferences} from "@capacitor/preferences";
export const useAuthStore = defineStore("auth", {
state: () => ({
user: null as null | { user_id: string; email: string; tenant_id?: string; role?: string },
profile: null as null | any,
tenants: [] as { tenant_id: string; role: string; tenants: { id: string; name: string } }[],
permissions: [] as string[],
activeTenant: null as any,
activeTenantData: null as any,
loading: true as boolean,
notLoggedIn: true,
}),
actions: {
async persist(token) {
if(useCapacitor().getIsNative()) {
console.log("On Native")
await Preferences.set({
key:"token",
value: token,
})
useCookie("token").value = token // persistieren
} else {
console.log("On Web")
useCookie("token").value = token // persistieren
}
},
async initStore() {
console.log("Auth initStore")
await this.fetchMe()
if(this.activeTenant > 0) {
this.loading = false
if(useCapacitor().getIsNative()) {
navigateTo("/mobile")
} else {
navigateTo("/")
}
}
},
async init(token=null) {
console.log("Auth init")
await this.fetchMe(token)
const tempStore = useTempStore()
if(this.profile.temp_config) tempStore.setStoredTempConfig(this.profile.temp_config)
if(this.activeTenant > 0) {
this.loading = false
if(useCapacitor().getIsNative()) {
navigateTo("/mobile")
} else {
navigateTo("/")
}
}
},
async login(email: string, password: string) {
try {
console.log("Auth login")
const { token } = await useNuxtApp().$api("/auth/login", {
method: "POST",
body: { email, password }
})
console.log("Token: " + token)
await this.fetchMe(token)
} catch (e) {
console.log("login error:" + e)
}
},
async logout() {
console.log("Auth logout")
try {
await useNuxtApp().$api("/auth/logout", { method: "POST" })
} catch (e) {
console.error("Logout fehlgeschlagen:", e)
}
this.user = null
this.permissions = []
this.profile = null
this.activeTenant = null
this.tenants = []
if(useCapacitor().getIsNative()) {
await Preferences.remove({ key: 'token' });
useCookie("token").value = null
} else {
useCookie("token").value = null
}
navigateTo("/login")
},
async fetchMe(jwt= null) {
console.log("Auth fetchMe")
const tempStore = useTempStore()
try {
const me = await useNuxtApp().$api("/api/me", {
headers: { Authorization: `Bearer ${jwt}`,
context: {
jwt
}}
})
console.log(me)
this.user = me.user
this.permissions = me.permissions
this.tenants = me.tenants
this.tenants.sort(function (a, b) {
if (a.id < b.id) return -1
if (a.id > b.id) return 1
})
this.profile = me.profile
if(this.profile.temp_config) tempStore.setStoredTempConfig(this.profile.temp_config)
if(me.activeTenant > 0) {
this.activeTenant = me.activeTenant
this.activeTenantData = me.tenants.find(i => i.id === me.activeTenant)
} else {
}
} catch (err: any) {
if (err?.response?.status === 401) this.logout()
}
},
async switchTenant(tenant_id: string) {
console.log("Auth switchTenant")
this.loading = true
const res = await useNuxtApp().$api("/api/tenant/switch", {
method: "POST",
body: { tenant_id }
})
console.log(res)
const {token} = res
if(useCapacitor().getIsNative()) {
await Preferences.set({
key:"token",
value: token,
})
} else {
useCookie("token").value = token // persistieren
}
await this.init(token)
},
hasPermission(key: string) {
return this.permissions.includes(key)
}
}
})

2639
frontend/stores/data.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,190 @@
import { defineStore } from "pinia"
import {
Utils,
RequestCommandId,
ResponseCommandId,
NiimbotBluetoothClient,
NiimbotSerialClient
} from "@mmote/niimbluelib"
import { useToast } from "#imports"
export const useLabelPrinterStore = defineStore("labelPrinter", {
state: () => ({
client: null as NiimbotBluetoothClient | NiimbotSerialClient | null,
connected: false,
connectLoading: false,
transportLastUsed: "",
printProgress: 0,
info: {} as any
}),
actions: {
/** Logging Helper */
logger(...args: any[]) {
console.debug("[Printer]", ...args)
},
/** --- Client erzeugen --- */
newClient(transport: "ble" | "serial" = "serial") {
const toast = useToast()
// alten Client trennen
if (this.client) {
try { this.client.disconnect() } catch {}
}
// neuen Client erzeugen
this.client =
transport === "ble"
? new NiimbotBluetoothClient()
: new NiimbotSerialClient()
/** Events registrieren */
this.client.on("printerinfofetched", (e) => {
console.log("printerInfoFetched")
console.log(e.info)
this.info = e.info
})
this.client.on("connect", () => {
this.connected = true
toast.add({ title: "Drucker verbunden" })
this.logger("connected")
})
this.client.on("disconnect", () => {
this.connected = false
toast.add({ title: "Drucker getrennt" })
this.logger("disconnected")
})
this.client.on("printprogress", (e) => {
if (e.pagePrintProgress) this.printProgress = e.pagePrintProgress
this.logger(
`Page ${e.page}/${e.pagesTotal}, Page print ${e.pagePrintProgress}%, Page feed ${e.pageFeedProgress}%`
)
})
return this.client
},
/** --- Verbinden --- */
async connect(transport: "ble" | "serial" = "serial") {
const toast = useToast()
this.connectLoading = true
this.newClient(transport)
try {
await this.client!.connect()
this.transportLastUsed = transport
this.connectLoading = false
} catch (err) {
console.error("[Printer] Connect failed:", err)
toast.add({ title: "Verbindung fehlgeschlagen", color: "red" })
this.connectLoading = false
}
},
/** --- Trennen --- */
async disconnect({ forget = false } = {}) {
const toast = useToast()
this.logger("Disconnect requested…")
if (!this.client) return
try {
// Timer stoppen
try {
if (this.client.heartbeatTimer) {
clearInterval(this.client.heartbeatTimer)
this.client.heartbeatTimer = null
}
if (this.client.abstraction?.statusPollTimer) {
clearInterval(this.client.abstraction.statusPollTimer)
this.client.abstraction.statusPollTimer = null
}
} catch {}
await this.client.disconnect?.()
// Serial-Port schließen
const port = (this.client as any).port
if (port) {
try {
if (port.readable) port.readable.cancel?.()
if (port.writable) await port.writable.abort?.()
await port.close?.()
if (forget && navigator.serial?.forgetPort) {
await navigator.serial.forgetPort(port)
}
} catch (err) {
this.logger("Error closing port:", err)
}
}
// BLE GATT
if (
this.client instanceof NiimbotBluetoothClient &&
this.client.device?.gatt?.connected
) {
try {
this.client.device.gatt.disconnect()
} catch {}
}
this.connected = false
this.client = null
toast.add({ title: "Drucker getrennt" })
} catch (err) {
console.error("[Printer] Disconnect error", err)
toast.add({ title: "Fehler beim Trennen", color: "red" })
}
},
/** Hilfsfunktion: EncodedImage reparieren */
reviveEncodedImage(encoded: any) {
if (!encoded?.rowsData) return encoded
for (const row of encoded.rowsData) {
if (row.rowData && !(row.rowData instanceof Uint8Array)) {
row.rowData = new Uint8Array(Object.values(row.rowData))
}
}
return encoded
},
/** --- Drucken --- */
async print(encoded: any, options?: { density?: number; pages?: number }) {
const toast = useToast()
if (!this.client) throw new Error("Kein Drucker verbunden")
const fixed = this.reviveEncodedImage(encoded)
const taskName = this.client.getPrintTaskType() ?? "B1"
const task = this.client.abstraction.newPrintTask(taskName, {
totalPages: options?.pages ?? 1,
statusPollIntervalMs: 100,
statusTimeoutMs: 8000,
density: options?.density ?? 5
})
try {
this.printProgress = 0
await task.printInit()
await task.printPage(fixed, options?.pages ?? 1)
await task.waitForFinished()
toast.add({ title: "Druck abgeschlossen" })
} catch (e) {
console.error("[Printer] print error", e)
toast.add({ title: "Druckfehler", color: "red" })
} finally {
await task.printEnd()
}
}
}
})

162
frontend/stores/profile.js Normal file
View File

@@ -0,0 +1,162 @@
import {defineStore} from 'pinia'
import OneSignal from "onesignal-cordova-plugin";
import {Capacitor} from "@capacitor/core";
// @ts-ignore
export const useProfileStore = defineStore('profile', () => {
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const user = useSupabaseUser()
const toast = useToast()
const router = useRouter()
const loaded = ref(false)
const showProfileSelection = ref(false)
const ownTenant = ref({
calendarConfig: {
eventTypes: []
},
timeConfig: {
timeTypes: []
},
tags: {
documents: [] ,
products: []
},
measures: []
})
const profiles = ref([])
const ownProfiles = ref([])
const activeProfile = ref([])
const tenants = ref([])
const currentTenant = ref(null)
async function initializeData (userId) {
let profileconnections = (await supabase.from("profileconnections").select()).data
let profiles = (await supabase.from("profiles").select("*, role(*)")).data
let activeProfileConnection = profileconnections.find(i => i.active)
if(activeProfileConnection) {
if(!await useCapacitor().getIsPhone()) {
toast.add({title: 'Aktives Profil ausgewählt'})
}
console.log("Active Profile selected")
activeProfile.value = profiles.find(i => i.id === activeProfileConnection.profile_id)
currentTenant.value = activeProfile.value.tenant
if(Capacitor.getPlatform() === "ios") {
OneSignal.initialize("1295d5ff-28f8-46a6-9c62-fe5d090016d7");
OneSignal.Location.setShared(false)
OneSignal.Notifications.requestPermission();
OneSignal.login(activeProfileConnection.user_id)
}
await fetchData()
await dataStore.fetchData()
} else {
toast.add({title: 'Kein aktives Profil', color: 'orange'})
await fetchOwnProfiles()
await fetchTenants()
showProfileSelection.value = true
}
}
async function changeProfile(newActiveProfileId) {
loaded.value = false
let profileconnections = (await supabase.from("profileconnections").select()).data
let oldActiveProfileConnection = profileconnections.find(i => i.active)
const {error} = await supabase.from("profileconnections").update({active: true}).eq("profile_id", newActiveProfileId)
if(error) {
console.log(error)
} else {
if(oldActiveProfileConnection){
const {error} = await supabase.from("profileconnections").update({active: false}).eq("profile_id", oldActiveProfileConnection.profile_id)
}
reloadNuxtApp({
path:"/",
ttl: 10000
})
}
}
async function fetchData () {
await fetchOwnProfiles()
await fetchProfiles()
await fetchTenants()
await fetchOwnTenant()
loaded.value = true
console.log("Finished Loading")
}
function clearStore () {
loaded.value = false
ownTenant.value = {}
profiles.value = []
ownProfiles.value = []
tenants.value = []
}
async function fetchOwnTenant () {
ownTenant.value = (await supabase.from("tenants").select().eq('id', currentTenant.value).single()).data
}
async function fetchProfiles () {
profiles.value = (await supabase.from("profiles").select().eq("tenant",currentTenant.value).order("lastName")).data
}
async function fetchOwnProfiles () {
let profiles = (await supabase.from("profiles").select().order("tenant")).data
let conns = (await supabase.from("profileconnections").select()).data.map(i => i.profile_id)
ownProfiles.value = profiles.filter(i => conns.includes(i.id))
}
async function fetchTenants () {
tenants.value = (await supabase.from("tenants").select().order("id",{ascending: true})).data
}
const getOwnProfile = computed(() => {
return profiles.value.find(i => i.id === user.value.id)
})
const getProfileById = computed(() => (itemId) => {
return profiles.value.find(item => item.id === itemId)
})
return {
//General
currentTenant,
loaded,
showProfileSelection,
ownTenant,
initializeData,
changeProfile,
//Data
profiles,
ownProfiles,
activeProfile,
tenants,
fetchData,
clearStore,
fetchOwnTenant,
getOwnProfile,
getProfileById
}
})

86
frontend/stores/temp.js Normal file
View File

@@ -0,0 +1,86 @@
import {defineStore} from 'pinia'
// @ts-ignore
export const useTempStore = defineStore('temp', () => {
const auth = useAuthStore()
const searchStrings = ref({})
const filters = ref({})
const columns = ref({})
const pages = ref({})
const settings = ref({})
const storeTempConfig = async () => {
const config = {
searchStrings: searchStrings.value,
columns: columns.value,
pages: pages.value,
settings: settings.value,
filters: filters.value
}
await useNuxtApp().$api(`/api/profiles/${auth.profile.id}`,{
method: 'PUT',
body: {temp_config: config}
})
}
function setStoredTempConfig (config) {
searchStrings.value = config.searchStrings
columns.value = config.columns
pages.value = config.pages
settings.value = config.settings
filters.value = config.filters || {}
}
function modifySearchString(type,input) {
searchStrings.value[type] = input
storeTempConfig()
}
function clearSearchString(type) {
searchStrings.value[type] = ""
storeTempConfig()
}
function modifyFilter(domain,type,input) {
if(!filters.value[domain]) filters.value[domain] = {}
filters.value[domain][type] = input
storeTempConfig()
}
function modifyColumns(type,input) {
columns.value[type] = input
storeTempConfig()
}
function modifyPages(type,input) {
pages.value[type] = input
storeTempConfig()
}
function modifySettings(type,input) {
settings.value[type] = input
storeTempConfig()
}
return {
setStoredTempConfig,
searchStrings,
modifySearchString,
clearSearchString,
filters,
modifyFilter,
columns,
modifyColumns,
modifyPages,
pages,
modifySettings,
settings
}
})