fynally fixed device assignation to user

This commit is contained in:
tdv
2025-10-08 11:58:52 +03:00
parent bd08dcc212
commit 7e2851e40a
5 changed files with 179 additions and 57 deletions

View File

@@ -25,8 +25,8 @@ const isTasksOpen = ref(false)
const itCertsOpen = ref(false) const itCertsOpen = ref(false)
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'refresh'): void (e: 'updated', payload: { name: string; userIds: string[] }): void
(e: 'error', err: unknown): void (e: 'deleted'): void
}>() }>()
function onDeleteConfirmed() { function onDeleteConfirmed() {
@@ -65,7 +65,7 @@ function onCertsConfirm() {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<EditDeviceDialog v-model:modelValue="isEditOpen" :device="props.row" :all-users="props.allUsers" @confirm="onEditConfirm" /> <EditDeviceDialog v-model:modelValue="isEditOpen" :device="props.row" :all-users="props.allUsers" @updated="onEditConfirm" />
<DeleteDeviceDialog v-model:modelValue="isDeleteOpen" :device="props.row" @confirm="onDeleteConfirmed" /> <DeleteDeviceDialog v-model:modelValue="isDeleteOpen" :device="props.row" @confirm="onDeleteConfirmed" />
<DeviceCertificateDialog v-model:modelValue="itCertsOpen" :device="props.row" @confirm="onCertsConfirm" /> <DeviceCertificateDialog v-model:modelValue="itCertsOpen" :device="props.row" @confirm="onCertsConfirm" />
<DeviceTasksDialog v-model:modelValue="isTasksOpen" :device="props.row" @confirm="onTaskConfirm" /> <DeviceTasksDialog v-model:modelValue="isTasksOpen" :device="props.row" @confirm="onTaskConfirm" />

View File

@@ -210,6 +210,7 @@ onBeforeUnmount(() => {
:dropdownComponent="AdminDeviceDropdown" :dropdownComponent="AdminDeviceDropdown"
:onRowUpdated="handleDeviceUpdated" :onRowUpdated="handleDeviceUpdated"
:onRowDeleted="handleDeviceDeleted" :onRowDeleted="handleDeviceDeleted"
:dropdownProps="{ allUsers: user_data }"
/> />
</TabsContent> </TabsContent>
@@ -220,6 +221,7 @@ onBeforeUnmount(() => {
:dropdownComponent="AdminDeviceDropdown" :dropdownComponent="AdminDeviceDropdown"
:onRowUpdated="handleTrackerUpdated" :onRowUpdated="handleTrackerUpdated"
:onRowDeleted="handleTrackerDeleted" :onRowDeleted="handleTrackerDeleted"
:dropdownProps="{ allUsers: user_data }"
/> />
</TabsContent> </TabsContent>
</Tabs> </Tabs>

View File

@@ -73,6 +73,16 @@ function onSelect(ev: CustomEvent) {
// When parent switches between external/internal data at runtime, refetch if needed. // When parent switches between external/internal data at runtime, refetch if needed.
watch(() => props.allUsers, () => { if (!usingExternal.value) loadUsers() }) watch(() => props.allUsers, () => { if (!usingExternal.value) loadUsers() })
// Debug: whenever modelValue changes (from dialog)
watch(() => props.modelValue, (v) => {
console.log('[AssignDevice:props.modelValue]', v)
}, { immediate: true })
// Debug: when effective users list is ready
watch(() => effectiveUsers.value, (v) => {
console.log('[AssignDevice:effectiveUsers]', v?.length, 'items')
})
</script> </script>
<template> <template>

View File

@@ -33,13 +33,14 @@ const props = defineProps<{
* :dropdownComponent="MyActionsMenu" * :dropdownComponent="MyActionsMenu"
* /> * />
*/ */
// dropdownComponent?: DefineComponent<{ row: TData }, any, any> dropdownComponent?: DefineComponent<{ row: TData }, any, any>
dropdownComponent?: DefineComponent<{ row: TData } & { // dropdownComponent?: DefineComponent<{ row: TData } & {
onRowUpdated?: (row: TData, payload: any) => void // onRowUpdated?: (row: TData, payload: any) => void
onRowDeleted?: (row: TData) => void // onRowDeleted?: (row: TData) => void
}, any, any> // }, any, any>
onRowUpdated?: (row: TData, payload: any) => void // <-- NEW // onRowUpdated?: (row: TData, payload: any) => void // <-- NEW
onRowDeleted?: (row: TData) => void // <-- NEW // onRowDeleted?: (row: TData) => void // <-- NEW
dropdownProps?: Record<string, any> // <-- NEW
}>() }>()
// ——— Table setup ——— // ——— Table setup ———
@@ -48,6 +49,11 @@ const table = useVueTable({
get columns() { return props.columns }, get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}) })
const emit = defineEmits<{
(e: 'row-updated', row: TData, payload: any): void
(e: 'row-deleted', row: TData): void
}>()
</script> </script>
<template> <template>
@@ -99,12 +105,19 @@ const table = useVueTable({
<!-- dropdown cell --> <!-- dropdown cell -->
<TableCell v-if="props.dropdownComponent" class="text-right"> <TableCell v-if="props.dropdownComponent" class="text-right">
<component <!-- <component
:is="props.dropdownComponent" :is="props.dropdownComponent"
:row="row.original" :row="row.original"
:key="row.id" :key="row.id"
:on-row-updated="props.onRowUpdated" :on-row-updated="props.onRowUpdated"
:on-row-deleted="props.onRowDeleted" :on-row-deleted="props.onRowDeleted"
/> -->
<component
:is="props.dropdownComponent"
:row="row.original"
v-bind="props.dropdownProps"
@updated="(payload: any) => emit('row-updated', row.original, payload)"
@deleted="() => emit('row-deleted', row.original)"
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@@ -1,61 +1,147 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
Dialog, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { defineProps, defineEmits, ref, watch, computed } from 'vue'
import type { PropType } from 'vue'
import AssignDevice from './AssignDevice.vue' import AssignDevice from './AssignDevice.vue'
import type { Device, Users } from '@/lib/interfaces' import type { Device, Users } from '@/lib/interfaces'
import { defineProps, defineEmits, ref, watch, computed } from 'vue'
import type { PropType } from 'vue'
import { api } from '@/lib/api' import { api } from '@/lib/api'
type DeviceWithUsers = type DeviceWithUsers =
& Partial<Device> & Partial<Device>
& { guid?: string; name?: string; devicename?: string } & { guid?: string; name?: string; devicename?: string; assigned_users?: string }
& { users?: Array<{ id: number; username: string; role?: string }> } & { users?: Array<{ id: number; username: string; role?: string }> }
// 1) runtime props so Vue + TS agree
const props = defineProps({ const props = defineProps({
modelValue: { type: Boolean as PropType<boolean>, required: true }, modelValue: { type: Boolean as PropType<boolean>, required: true },
device: { type: Object as PropType<DeviceWithUsers>, required: true }, // must have guid device: { type: Object as PropType<DeviceWithUsers>, required: true },
allUsers: { type: Array as PropType<Users[]>, required: false }, allUsers: { type: Array as PropType<Users[]>, required: false },
}) })
// 2) two emits: v-model and confirm
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void (e: 'update:modelValue', v: boolean): void
// (e: 'confirm'): void
(e: 'updated', payload: { name: string; userIds: string[] }): void (e: 'updated', payload: { name: string; userIds: string[] }): void
}>() }>()
// ---------- Local form state ----------
const guid = computed(() => String(props.device?.guid ?? '')) const guid = computed(() => String(props.device?.guid ?? ''))
const originalName = computed(() => (props.device?.devicename ?? props.device?.name ?? '')) const originalName = computed(() => (props.device?.devicename ?? props.device?.name ?? ''))
const originalIds = computed<string[]>(() =>
Array.isArray(props.device?.users) ? props.device!.users.map(u => String(u.id)) : []
)
const originalIds = ref<string[]>([]) // baseline for compare
const name = ref('') const name = ref('')
const selectedUserIds = ref<string[]>([]) const selectedUserIds = ref<string[]>([]) // live edited selection
// ----- init control: only init once per open/guid -----
const initedForGuid = ref<string | null>(null)
function usernameToId(u: string): string | null {
const id = props.allUsers?.find(x => x.username === u)?.id
return typeof id === 'number' ? String(id) : null
}
function initFromDevice() {
const dev = props.device
name.value = originalName.value
if (Array.isArray(dev?.users) && dev!.users.length) {
originalIds.value = dev!.users.map(u => String(u.id))
} else {
// Map from assigned_users (comma-separated usernames) -> ids (requires allUsers)
const usernames = (dev?.assigned_users ?? '')
.split(',')
.map(s => s.trim())
.filter(Boolean)
if (usernames.length && props.allUsers?.length) {
originalIds.value = usernames
.map(usernameToId)
.filter((x): x is string => !!x)
} else {
originalIds.value = []
}
}
selectedUserIds.value = [...originalIds.value]
initedForGuid.value = guid.value
console.log('[EditDeviceDialog:init]', {
guid: guid.value,
originalName: originalName.value,
originalIds: [...originalIds.value],
})
}
// When dialog opens, initialize once for this guid
watch( watch(
() => props.device, () => props.modelValue,
() => { (open) => {
name.value = originalName.value if (open && guid.value && initedForGuid.value !== guid.value) {
selectedUserIds.value = [...originalIds.value] initFromDevice()
}
if (!open) {
// reset flag so next open re-inits
initedForGuid.value = null
}
}, },
{ immediate: true } { immediate: true }
) )
// ---------- Save (conditional API calls) ---------- // If allUsers arrives later AND we haven't successfully initialized for this guid,
// try again (but only when open).
// watch(
// () => props.allUsers,
// () => {
// if (props.modelValue && guid.value && initedForGuid.value !== guid.value) {
// initFromDevice()
// }
// }
// )
// If allUsers arrives later, and we couldn't map on first init, map now.
watch(
() => props.allUsers,
() => {
if (!props.modelValue || !guid.value) return
const dev = props.device
const hasUsersArray = Array.isArray(dev?.users) && dev!.users.length > 0
const hasAssigned = typeof dev?.assigned_users === 'string' && dev!.assigned_users.trim().length > 0
const canMapNow = Array.isArray(props.allUsers) && props.allUsers.length > 0
// Only re-map if we *didn't* have users[], we *do* have assigned usernames,
// we *couldn't* map earlier (originalIds is empty), and now we have allUsers.
if (!hasUsersArray && hasAssigned && originalIds.value.length === 0 && canMapNow) {
const usernames = dev!.assigned_users!
.split(',')
.map(s => s.trim())
.filter(Boolean)
const mapped = usernames
.map(u => {
const id = props.allUsers!.find(x => x.username === u)?.id
return typeof id === 'number' ? String(id) : null
})
.filter((x): x is string => !!x)
if (mapped.length > 0) {
originalIds.value = mapped
selectedUserIds.value = [...mapped]
console.log('[EditDeviceDialog:remap-after-allUsers]', { mapped })
}
}
},
{ immediate: false }
)
// Just to see live edits from AssignDevice
watch(selectedUserIds, (v) => {
console.log('[EditDeviceDialog:selectedUserIds]', [...v])
}, { deep: true })
// ---------- Save ----------
const saving = ref(false) const saving = ref(false)
const errorText = ref<string | null>(null) const errorText = ref<string | null>(null)
@@ -63,7 +149,6 @@ function changedName() {
return name.value.trim() !== originalName.value.trim() return name.value.trim() !== originalName.value.trim()
} }
function changedUsers() { function changedUsers() {
// compare as sets
const a = new Set(originalIds.value) const a = new Set(originalIds.value)
const b = new Set(selectedUserIds.value) const b = new Set(selectedUserIds.value)
if (a.size !== b.size) return true if (a.size !== b.size) return true
@@ -76,26 +161,31 @@ async function onSave() {
errorText.value = null errorText.value = null
saving.value = true saving.value = true
try { try {
console.log('[EditDeviceDialog:compare]', {
originalIds: [...originalIds.value],
selectedUserIds: [...selectedUserIds.value],
})
const ops: Promise<any>[] = [] const ops: Promise<any>[] = []
if (changedName()) { const nameChanged = changedName()
ops.push( const usersChanged = changedUsers()
api.post(`/devices/${encodeURIComponent(guid.value)}/rename`, { console.log('[EditDeviceDialog:onSave]', { nameChanged, usersChanged, name: name.value })
name: name.value.trim(),
} as { name: string }) if (nameChanged) {
) ops.push(api.post(`/devices/${encodeURIComponent(guid.value)}/rename`, {
name: name.value.trim(),
} as { name: string }))
} }
if (changedUsers()) { if (usersChanged) {
const userIdsNum = selectedUserIds.value const userIdsNum = selectedUserIds.value
.map(v => Number(v)) .map(v => Number(v))
.filter(n => Number.isFinite(n)) as number[] .filter(n => Number.isFinite(n)) as number[]
ops.push( ops.push(api.post(`/devices/${encodeURIComponent(guid.value)}/set_users`, {
api.post(`/devices/${encodeURIComponent(guid.value)}/set_users`, { userIds: userIdsNum,
userIds: userIdsNum, } as { userIds: number[] }))
} as { userIds: number[] })
)
} }
if (ops.length > 0) { if (ops.length > 0) {
@@ -105,7 +195,7 @@ async function onSave() {
emit('updated', { name: name.value, userIds: [...selectedUserIds.value] }) emit('updated', { name: name.value, userIds: [...selectedUserIds.value] })
emit('update:modelValue', false) emit('update:modelValue', false)
} catch (err: any) { } catch (err: any) {
console.error(err) console.error('[EditDeviceDialog:onSave] error', err)
errorText.value = err?.response?.data?.message || 'Failed to save changes.' errorText.value = err?.response?.data?.message || 'Failed to save changes.'
} finally { } finally {
saving.value = false saving.value = false
@@ -114,30 +204,37 @@ async function onSave() {
</script> </script>
<template> <template>
<Dialog :open="props.modelValue" @update:open="(v: boolean) => emit('update:modelValue', v)"> <Dialog :open="props.modelValue" @update:open="(v:boolean)=>emit('update:modelValue', v)">
<DialogContent class="sm:min-w-[500px]"> <DialogContent class="sm:min-w-[500px]">
<DialogHeader> <DialogHeader>
<DialogTitle>Edit device</DialogTitle> <DialogTitle>Edit device</DialogTitle>
<DialogDescription> <DialogDescription>Make changes to device settings below.</DialogDescription>
Make changes to device settings below.
</DialogDescription>
</DialogHeader> </DialogHeader>
<div class="grid gap-4 py-4"> <div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4"> <div class="grid grid-cols-4 items-center gap-4">
<Label for="guid" class="text-right">GUID</Label> <Label for="guid" class="text-right">GUID</Label>
<p id="guid" class="col-span-3 break-all text-muted-foreground"> {{ props.device?.guid }}</p> <p id="guid" class="col-span-3 break-all text-muted-foreground">{{ guid }}</p>
</div> </div>
<div class="grid grid-cols-4 items-center gap-4"> <div class="grid grid-cols-4 items-center gap-4">
<Label for="name" class="text-right">Name</Label> <Label for="name" class="text-right">Name</Label>
<Input id="name" class="col-span-3" v-model="name" :disabled="saving" placeholder="Device name"/> <Input id="name" class="col-span-3" v-model="name" :disabled="saving" placeholder="Device name" />
</div> </div>
<div class="grid grid-cols-4 items-center gap-4"> <div class="grid grid-cols-4 items-center gap-4">
<Label for="users" class="text-right">Allowed users</Label> <Label for="users" class="text-right">Allowed users</Label>
<AssignDevice id="users" v-model="selectedUserIds" :all-users="props.allUsers" class="col-span-3 w-full" /> <AssignDevice
id="users"
class="col-span-3 w-full"
v-model="selectedUserIds"
:all-users="props.allUsers"
/>
</div> </div>
<p v-if="errorText" class="text-sm text-red-600 mt-1 col-span-4">{{ errorText }}</p> <p v-if="errorText" class="text-sm text-red-600 mt-1 col-span-4">{{ errorText }}</p>
</div> </div>
<DialogFooter> <DialogFooter>
<Button :disabled="saving" @click="onSave"> <Button :disabled="saving" @click="onSave">
{{ saving ? 'Saving' : 'Save changes' }} {{ saving ? 'Saving' : 'Save changes' }}
@@ -145,4 +242,4 @@ async function onSave() {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</template> </template>