Skip to content

Commit e7b403e

Browse files
committed
fix(web): corrige LiveMap, StaffChat, Settings, inventario, sync e diversos de UI (#62)
1 parent bd1cd13 commit e7b403e

11 files changed

Lines changed: 96 additions & 24 deletions

File tree

web/src/components/Listeners.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export default function Listeners() {
173173
off('showEntityInfo', showEntityInfo)
174174
off('showNearbyEntities', showNearbyEntities)
175175
off('setMessages', setMessages)
176+
off('updatePermissions', setPermissions)
176177
}
177178

178179
}, [on, off, setPlayers, setVehicleDev, setShowCoords, setEntityInfo, setNearbyEntities, setGameData, sendNui, setLastPlayersFetch, setMyPermissions, setPagination, setStaffMessages, setSupportedLanguages, setPermissionDefinitions, setCategoryDefinitions])

web/src/components/map/LiveMap.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,17 @@ function MapResetter({ resetTrigger, initialZoom }: { resetTrigger: number, init
154154

155155
function MapController({ centerOnMarkerId, markers }: { centerOnMarkerId?: string | number | null, markers: MapMarker[] }) {
156156
const map = useMap()
157+
// Keep the latest markers in a ref so we can look up the target without
158+
// re-running the fly-to effect on every poll (markers is a new array each poll).
159+
const markersRef = useRef(markers)
160+
markersRef.current = markers
157161
useEffect(() => {
158-
if (centerOnMarkerId && markers.length > 0) {
159-
const target = markers.find(m => String(m.id) === String(centerOnMarkerId))
160-
if (target) {
161-
map.flyTo([target.y, target.x], 3)
162-
}
162+
if (!centerOnMarkerId) return
163+
const target = markersRef.current.find(m => String(m.id) === String(centerOnMarkerId))
164+
if (target) {
165+
map.flyTo([target.y, target.x], 3)
163166
}
164-
}, [centerOnMarkerId, markers, map])
167+
}, [centerOnMarkerId, map])
165168
return null
166169
}
167170

@@ -259,10 +262,11 @@ export default function LiveMap({
259262
// Effect to handle player selection from props
260263
useEffect(() => {
261264
if (selectedPlayerId) {
262-
setTimeout(() => {
265+
const timer = setTimeout(() => {
263266
const marker = markerRefs.current[selectedPlayerId]
264267
if (marker) marker.openPopup()
265268
}, 300)
269+
return () => clearTimeout(timer)
266270
} else {
267271
Object.values(markerRefs.current).forEach(marker => {
268272
if (marker && marker.closePopup) marker.closePopup()

web/src/components/players/InventoryItem.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ export const InventoryItem: React.FC<InventoryItemProps> = ({
6161
const imageUrls = getImageUrl(name)
6262
const [currentImageIndex, setCurrentImageIndex] = React.useState(0)
6363

64+
// Reset image fallback state when the item occupying this slot changes,
65+
// otherwise a previous item's error/index leaks into the new item.
66+
React.useEffect(() => {
67+
setImageError(false)
68+
setCurrentImageIndex(0)
69+
}, [name])
70+
6471
const handleImageError = () => {
6572
if (currentImageIndex < imageUrls.length - 1) {
6673
setCurrentImageIndex(prev => prev + 1)

web/src/components/players/InventoryViewerModal.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,13 @@ const InventorySlot = ({ target, onClose, onAddComparison, isSecondary, otherTar
120120
const handleNuiMessage = (event: MessageEvent) => {
121121
const { action, data: eventData } = event.data;
122122
if (action === 'inventoryUpdate') {
123-
const invId = target.type === 'player' ? Number(target.id) : (target.type === 'trunk' ? 'trunk' + target.id : 'glovebox' + target.id);
123+
const invId = target.type === 'player'
124+
? Number(target.id)
125+
: target.type === 'trunk'
126+
? 'trunk' + target.id
127+
: target.type === 'chest'
128+
? 'chest' + target.id
129+
: 'glovebox' + target.id;
124130
if (eventData.inventoryId === invId) {
125131
fetchInventory();
126132
}
@@ -409,7 +415,7 @@ const InventorySlot = ({ target, onClose, onAddComparison, isSecondary, otherTar
409415
return slots
410416
}
411417

412-
const weightPercent = data ? Math.min(100, (data.weight / data.maxWeight) * 100) : 0
418+
const weightPercent = data && data.maxWeight > 0 ? Math.min(100, (data.weight / data.maxWeight) * 100) : 0
413419
const formatTotalWeight = (w: number) => (w / 1000).toFixed(1)
414420

415421
return (

web/src/components/players/ScreenModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export default function ScreenModal({ playerId, onClose, playerName }: ScreenMod
6262
// Normalize for MriPlayerVitals component
6363
setVitals({
6464
...v,
65-
health: v.health || 100,
65+
health: v.health ?? 100,
6666
metadata: {
6767
...v.metadata,
6868
hunger: v.metadata?.hunger ?? 100,

web/src/components/vip/AddVipModal.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react'
1+
import { useState, useEffect } from 'react'
22
import { MriActionModal, MriInput, MriSelect, MriDatePicker } from '@mriqbox/ui-kit'
33
import { Crown } from 'lucide-react'
44
import { useI18n } from '@/hooks/useI18n'
@@ -54,6 +54,16 @@ export default function AddVipModal({ ranks = [], onClose, onSubmit }: Props) {
5454
{ label: t('player.actions.money.types.crypto'), value: 'crypto' },
5555
]
5656

57+
// Seed defaults once ranks arrive async (not only on first mount, when ranks is still empty)
58+
useEffect(() => {
59+
if (ranks.length === 0 || rankId) return
60+
const first = ranks[0]
61+
setRankId(first.id)
62+
setSalary(first.salary)
63+
setSalaryType(first.salaryType)
64+
setInventoryLimit(first.inventoryLimit)
65+
}, [ranks, rankId])
66+
5767
const rankOptions = ranks.map(r => ({ label: r.label, value: r.id }))
5868

5969
const handleRankChange = (id: string) => {
@@ -63,6 +73,9 @@ export default function AddVipModal({ ranks = [], onClose, onSubmit }: Props) {
6373
}
6474

6575
const handleConfirm = () => {
76+
// Reject empty identifier for the selected method
77+
if (method === 'citizenid' && !citizenid.trim()) return
78+
if (method === 'source' && !source.trim()) return
6679
let expiration = 0
6780
if (duration === 'custom') {
6881
if (!customDate) return

web/src/pages/Dashboard/Dashboard.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,11 +338,15 @@ export default function Dashboard() {
338338
}, 500)
339339
return () => {
340340
mounted = false
341+
abortSyncRef.current = true
341342
clearTimeout(timer)
342343
}
343344
} else {
344345
fetchPlayers()
345-
return () => { mounted = false }
346+
return () => {
347+
mounted = false
348+
abortSyncRef.current = true
349+
}
346350
}
347351
// eslint-disable-next-line react-hooks/exhaustive-deps
348352
}, [sendNui, playersSearch, lastPlayersFetch, setPlayers, setPagination, setLastPlayersFetch, syncRemainingPages])
@@ -566,7 +570,12 @@ export default function Dashboard() {
566570
onChange={(e) => setAnnouncement(e.target.value)}
567571
placeholder={t('qadmin.dashboard.announcement.placeholder')}
568572
className="flex-1 bg-muted/30 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary/50 transition-all placeholder:text-muted-foreground/50"
569-
onKeyDown={(e) => e.key === 'Enter' && handleSendAnnounce()}
573+
onKeyDown={(e) => {
574+
if (e.key === 'Enter' && !e.shiftKey) {
575+
e.preventDefault()
576+
if (!sendingAnnounce) handleSendAnnounce()
577+
}
578+
}}
570579
/>
571580
<MriButton
572581
onClick={handleSendAnnounce}

web/src/pages/Logs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1054,7 +1054,7 @@ export default function Logs() {
10541054

10551055
const handleExport = () => {
10561056
const content = logs.map(l =>
1057-
`[${formatTime(l.created_at)}] [${l.level.toUpperCase()}] [${l.category}] ${l.resource} | ${l.message} | by ${l.admin}`
1057+
`[${formatTime(l.created_at)}] [${(l.level || 'info').toUpperCase()}] [${l.category}] ${l.resource} | ${l.message} | by ${l.admin}`
10581058
).join('\n')
10591059
const blob = new Blob([content], { type: 'text/plain' })
10601060
const url = URL.createObjectURL(blob)

web/src/pages/Players.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ export default function Players() {
126126
const [stashInput, setStashInput] = useState('')
127127

128128
const abortSyncRef = useRef(false)
129+
const syncTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
130+
131+
// Abort the background page-sync recursion and clear its pending timer on unmount
132+
useEffect(() => () => {
133+
abortSyncRef.current = true
134+
if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current)
135+
}, [])
129136

130137
const getPlayerKey = useCallback((p: any) => {
131138
if (!p) return t('common.unknown')
@@ -226,7 +233,8 @@ export default function Players() {
226233
if (pageToFetch === 1) {
227234
setPlayers(data)
228235
if (searchQuery === '' && pagesCount > 1) {
229-
setTimeout(() => syncRemainingPages(2, pagesCount, ''), 1000)
236+
if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current)
237+
syncTimeoutRef.current = setTimeout(() => syncRemainingPages(2, pagesCount, ''), 1000)
230238
}
231239
if (searchQuery === '') {
232240
setLastPlayersFetch(Date.now())
@@ -882,7 +890,7 @@ export default function Players() {
882890
else if (mName === 'black_money') color = "text-red-500";
883891

884892
const isCurrency = ['cash', 'bank', 'black_money'].includes(mName);
885-
const labelKey = `option_${mName} `;
893+
const labelKey = `option_${mName}`;
886894
const translated = t(labelKey);
887895
const label = translated !== labelKey ? translated : mName;
888896

@@ -1121,7 +1129,7 @@ export default function Players() {
11211129
availableTypes={
11221130
Array.isArray(selectedPlayer.money)
11231131
? selectedPlayer.money.map((m: any) => {
1124-
const labelRaw = t(`option_${m.name} `) !== `option_${m.name} ` ? t(`option_${m.name} `) : m.name;
1132+
const labelRaw = t(`option_${m.name}`) !== `option_${m.name}` ? t(`option_${m.name}`) : m.name;
11251133
return {
11261134
label: labelRaw.charAt(0).toUpperCase() + labelRaw.slice(1),
11271135
value: m.name
@@ -1205,7 +1213,7 @@ export default function Players() {
12051213
onChange={(e) => setStashInput((e.target as HTMLInputElement).value)}
12061214
placeholder="ex: police_stash_1"
12071215
className="bg-background border-border h-10"
1208-
onKeyDown={(e) => { if (e.key === 'Enter' && stashInput.trim()) { sendAction({ event: 'mri_Qadmin:client:openStash', type: 'client' }, { Stash: { value: stashInput.trim() } }); setShowOpenStashModal(false); setStashInput('') } }}
1216+
onKeyDown={(e) => { if (e.key === 'Enter' && stashInput.trim()) { sendAction({ event: 'mri_Qadmin:client:openStash', type: 'client', perms: 'qadmin.action.open_stash' }, { Stash: { value: stashInput.trim() } }); setShowOpenStashModal(false); setStashInput('') } }}
12091217
/>
12101218
</div>
12111219
</MriActionModal>

web/src/pages/Settings.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@ const WALL_DEFAULTS = {
1818
}
1919

2020
const hexToRgb = (hex: string) => {
21-
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
21+
if (!hex) return '0, 0, 255';
22+
let h = hex.replace(/^#/, '');
23+
// Support shorthand 3-digit hex (#FFF -> #FFFFFF)
24+
if (h.length === 3) h = h.split('').map(c => c + c).join('');
25+
const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(h);
2226
return result ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}` : '0, 0, 255';
2327
}
2428

2529
const rgbToHex = (rgb: string) => {
2630
if (!rgb) return '#0000FF';
2731
if (rgb.startsWith('#')) return rgb;
2832
const parts = rgb.split(',').map(x => parseInt(x.trim()));
29-
if (parts.length < 3) return '#0000FF';
33+
if (parts.length < 3 || parts.some(n => Number.isNaN(n))) return '#0000FF';
3034
const [r, g, b] = parts;
3135
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
3236
}
@@ -120,6 +124,11 @@ export default function Settings() {
120124

121125
const timeoutRef = React.useRef<Record<string, any>>({})
122126

127+
// Clear any pending debounced wall-color saves on unmount
128+
React.useEffect(() => () => {
129+
Object.values(timeoutRef.current).forEach(clearTimeout)
130+
}, [])
131+
123132
const handleLocalWallChange = (type: 'global' | 'principal', key: string, value: string) => {
124133
setLocalWallSettings((prev: any) => {
125134
const next = { ...prev }
@@ -177,7 +186,18 @@ export default function Settings() {
177186
try {
178187
const rgbValue = hexToRgb(value);
179188
await sendNui('mri_Qadmin:server:SaveWallSetting', { type, key, value: rgbValue })
180-
await fetchWallSettings() // Sync back from server
189+
// Update ONLY the saved key from our confirmed value instead of
190+
// refetching-and-replacing the whole object. A full replace here
191+
// races with rapid picker changes and clobbers the value the user
192+
// is actively editing on a different key.
193+
const applyKey = (obj: any) => {
194+
const next = { ...obj }
195+
if (type === 'global') next.settings = { ...next.settings, [key]: value }
196+
else next.colors = { ...next.colors, [key]: value }
197+
return next
198+
}
199+
setWallSettings((prev: any) => applyKey(prev))
200+
setLocalWallSettings((prev: any) => applyKey(prev))
181201
} finally {
182202
setSaving(false)
183203
}

0 commit comments

Comments
 (0)