feat: add configurable permissions system with admin panel
Adds a full permissions management feature allowing admins to control who can perform actions across the app (trip CRUD, files, places, budget, packing, reservations, collab, members, share links). - New server/src/services/permissions.ts: 16 configurable actions, in-memory cache, checkPermission() helper, backwards-compatible defaults matching upstream behaviour - GET/PUT /admin/permissions endpoints; permissions loaded into app-config response so clients have them on startup - checkPermission() applied to all mutating route handlers across 10 server route files; getTripOwnerId() helper eliminates repeated inline DB queries; trips.ts and files.ts now reuse canAccessTrip() result to avoid redundant DB round-trips - New client/src/store/permissionsStore.ts: Zustand store + useCanDo() hook; TripOwnerContext type accepts both Trip and DashboardTrip shapes without casting at call sites - New client/src/components/Admin/PermissionsPanel.tsx: categorised UI with per-action dropdowns, customised badge, save/reset - AdminPage, DashboardPage, FileManager, PlacesSidebar, TripMembersModal gated via useCanDo(); no prop drilling - 46 perm.* translation keys added to all 12 language files
This commit is contained in:
@@ -14,6 +14,7 @@ import SharedTripPage from './pages/SharedTripPage'
|
||||
import { ToastContainer } from './components/shared/Toast'
|
||||
import { TranslationProvider, useTranslation } from './i18n'
|
||||
import { authApi } from './api/client'
|
||||
import { usePermissionsStore, PermissionLevel } from './store/permissionsStore'
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: ReactNode
|
||||
@@ -21,7 +22,10 @@ interface ProtectedRouteProps {
|
||||
}
|
||||
|
||||
function ProtectedRoute({ children, adminRequired = false }: ProtectedRouteProps) {
|
||||
const { isAuthenticated, user, isLoading, appRequireMfa } = useAuthStore()
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
|
||||
const user = useAuthStore((s) => s.user)
|
||||
const isLoading = useAuthStore((s) => s.isLoading)
|
||||
const appRequireMfa = useAuthStore((s) => s.appRequireMfa)
|
||||
const { t } = useTranslation()
|
||||
const location = useLocation()
|
||||
|
||||
@@ -78,12 +82,13 @@ export default function App() {
|
||||
if (token) {
|
||||
loadUser()
|
||||
}
|
||||
authApi.getAppConfig().then(async (config: { demo_mode?: boolean; has_maps_key?: boolean; version?: string; timezone?: string; require_mfa?: boolean; trip_reminders_enabled?: boolean }) => {
|
||||
authApi.getAppConfig().then(async (config: { demo_mode?: boolean; has_maps_key?: boolean; version?: string; timezone?: string; require_mfa?: boolean; trip_reminders_enabled?: boolean; permissions?: Record<string, PermissionLevel> }) => {
|
||||
if (config?.demo_mode) setDemoMode(true)
|
||||
if (config?.has_maps_key !== undefined) setHasMapsKey(config.has_maps_key)
|
||||
if (config?.timezone) setServerTimezone(config.timezone)
|
||||
if (config?.require_mfa !== undefined) setAppRequireMfa(!!config.require_mfa)
|
||||
if (config?.trip_reminders_enabled !== undefined) setTripRemindersEnabled(config.trip_reminders_enabled)
|
||||
if (config?.permissions) usePermissionsStore.getState().setPermissions(config.permissions)
|
||||
|
||||
if (config?.version) {
|
||||
const storedVersion = localStorage.getItem('trek_app_version')
|
||||
|
||||
@@ -184,6 +184,8 @@ export const adminApi = {
|
||||
apiClient.get('/admin/audit-log', { params }).then(r => r.data),
|
||||
mcpTokens: () => apiClient.get('/admin/mcp-tokens').then(r => r.data),
|
||||
deleteMcpToken: (id: number) => apiClient.delete(`/admin/mcp-tokens/${id}`).then(r => r.data),
|
||||
getPermissions: () => apiClient.get('/admin/permissions').then(r => r.data),
|
||||
updatePermissions: (permissions: Record<string, string>) => apiClient.put('/admin/permissions', { permissions }).then(r => r.data),
|
||||
}
|
||||
|
||||
export const addonsApi = {
|
||||
|
||||
169
client/src/components/Admin/PermissionsPanel.tsx
Normal file
169
client/src/components/Admin/PermissionsPanel.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react'
|
||||
import { adminApi } from '../../api/client'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { usePermissionsStore, PermissionLevel } from '../../store/permissionsStore'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import { Save, Loader2, RotateCcw } from 'lucide-react'
|
||||
import CustomSelect from '../shared/CustomSelect'
|
||||
|
||||
interface PermissionEntry {
|
||||
key: string
|
||||
level: PermissionLevel
|
||||
defaultLevel: PermissionLevel
|
||||
allowedLevels: PermissionLevel[]
|
||||
}
|
||||
|
||||
const LEVEL_LABELS: Record<string, string> = {
|
||||
admin: 'perm.level.admin',
|
||||
trip_owner: 'perm.level.tripOwner',
|
||||
trip_member: 'perm.level.tripMember',
|
||||
everybody: 'perm.level.everybody',
|
||||
}
|
||||
|
||||
const CATEGORIES = [
|
||||
{ id: 'trip', keys: ['trip_create', 'trip_edit', 'trip_delete', 'trip_archive', 'trip_cover_upload'] },
|
||||
{ id: 'members', keys: ['member_manage'] },
|
||||
{ id: 'files', keys: ['file_upload', 'file_edit', 'file_delete'] },
|
||||
{ id: 'content', keys: ['place_edit', 'day_edit', 'reservation_edit'] },
|
||||
{ id: 'extras', keys: ['budget_edit', 'packing_edit', 'collab_edit', 'share_manage'] },
|
||||
]
|
||||
|
||||
export default function PermissionsPanel(): React.ReactElement {
|
||||
const { t } = useTranslation()
|
||||
const toast = useToast()
|
||||
const [entries, setEntries] = useState<PermissionEntry[]>([])
|
||||
const [values, setValues] = useState<Record<string, PermissionLevel>>({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [dirty, setDirty] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadPermissions()
|
||||
}, [])
|
||||
|
||||
const loadPermissions = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await adminApi.getPermissions()
|
||||
setEntries(data.permissions)
|
||||
const vals: Record<string, PermissionLevel> = {}
|
||||
for (const p of data.permissions) vals[p.key] = p.level
|
||||
setValues(vals)
|
||||
setDirty(false)
|
||||
} catch {
|
||||
toast.error(t('common.error'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (key: string, level: PermissionLevel) => {
|
||||
setValues(prev => ({ ...prev, [key]: level }))
|
||||
setDirty(true)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
const data = await adminApi.updatePermissions(values)
|
||||
if (data.permissions) {
|
||||
usePermissionsStore.getState().setPermissions(data.permissions)
|
||||
}
|
||||
setDirty(false)
|
||||
toast.success(t('perm.saved'))
|
||||
} catch {
|
||||
toast.error(t('common.error'))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
const defaults: Record<string, PermissionLevel> = {}
|
||||
for (const p of entries) defaults[p.key] = p.defaultLevel
|
||||
setValues(defaults)
|
||||
setDirty(true)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-8 text-center">
|
||||
<div className="w-8 h-8 border-2 border-slate-200 border-t-slate-900 rounded-full animate-spin mx-auto" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const entryMap = useMemo(() => new Map(entries.map(e => [e.key, e])), [entries])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="font-semibold text-slate-900">{t('perm.title')}</h2>
|
||||
<p className="text-xs text-slate-400 mt-0.5">{t('perm.subtitle')}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleReset}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-sm border border-slate-300 rounded-lg hover:bg-slate-50 disabled:opacity-40 transition-colors"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
{t('perm.resetDefaults')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving || !dirty}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-slate-900 text-white rounded-lg hover:bg-slate-700 disabled:bg-slate-400 transition-colors"
|
||||
>
|
||||
{saving ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
|
||||
{t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-100">
|
||||
{CATEGORIES.map(cat => (
|
||||
<div key={cat.id} className="px-6 py-4">
|
||||
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">
|
||||
{t(`perm.cat.${cat.id}`)}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{cat.keys.map(key => {
|
||||
const entry = entryMap.get(key)
|
||||
if (!entry) return null
|
||||
const currentLevel = values[key] || entry.defaultLevel
|
||||
const isDefault = currentLevel === entry.defaultLevel
|
||||
return (
|
||||
<div key={key} className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-slate-700">{t(`perm.action.${key}`)}</p>
|
||||
<p className="text-xs text-slate-400 mt-0.5">{t(`perm.actionHint.${key}`)}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{!isDefault && (
|
||||
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-amber-100 text-amber-700">
|
||||
{t('perm.customized')}
|
||||
</span>
|
||||
)}
|
||||
<CustomSelect
|
||||
value={currentLevel}
|
||||
onChange={(val) => handleChange(key, val as PermissionLevel)}
|
||||
options={entry.allowedLevels.map(l => ({
|
||||
value: l,
|
||||
label: t(LEVEL_LABELS[l] || l),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { useToast } from '../shared/Toast'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { filesApi } from '../../api/client'
|
||||
import type { Place, Reservation, TripFile, Day, AssignmentsMap } from '../../types'
|
||||
import { useCanDo } from '../../store/permissionsStore'
|
||||
import { useTripStore } from '../../store/tripStore'
|
||||
|
||||
function authUrl(url: string): string {
|
||||
const token = localStorage.getItem('auth_token')
|
||||
@@ -159,6 +161,8 @@ export default function FileManager({ files = [], onUpload, onDelete, onUpdate,
|
||||
const [trashFiles, setTrashFiles] = useState<TripFile[]>([])
|
||||
const [loadingTrash, setLoadingTrash] = useState(false)
|
||||
const toast = useToast()
|
||||
const can = useCanDo()
|
||||
const trip = useTripStore((s) => s.trip)
|
||||
const { t, locale } = useTranslation()
|
||||
|
||||
const loadTrash = useCallback(async () => {
|
||||
@@ -710,7 +714,7 @@ export default function FileManager({ files = [], onUpload, onDelete, onUpdate,
|
||||
) : (
|
||||
<>
|
||||
{/* Upload zone */}
|
||||
<div
|
||||
{can('file_upload', trip) && <div
|
||||
{...getRootProps()}
|
||||
style={{
|
||||
margin: '16px 16px 0', border: '2px dashed', borderRadius: 14, padding: '20px 16px',
|
||||
@@ -735,7 +739,7 @@ export default function FileManager({ files = [], onUpload, onDelete, onUpdate,
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{/* Filter tabs */}
|
||||
<div style={{ display: 'flex', gap: 4, padding: '12px 16px 0', flexShrink: 0, flexWrap: 'wrap' }}>
|
||||
|
||||
@@ -11,6 +11,7 @@ import CustomSelect from '../shared/CustomSelect'
|
||||
import { useContextMenu, ContextMenu } from '../shared/ContextMenu'
|
||||
import { placesApi } from '../../api/client'
|
||||
import { useTripStore } from '../../store/tripStore'
|
||||
import { useCanDo } from '../../store/permissionsStore'
|
||||
import type { Place, Category, Day, AssignmentsMap } from '../../types'
|
||||
|
||||
interface PlacesSidebarProps {
|
||||
@@ -38,7 +39,10 @@ const PlacesSidebar = React.memo(function PlacesSidebar({
|
||||
const toast = useToast()
|
||||
const ctxMenu = useContextMenu()
|
||||
const gpxInputRef = useRef<HTMLInputElement>(null)
|
||||
const tripStore = useTripStore()
|
||||
const trip = useTripStore((s) => s.trip)
|
||||
const loadTrip = useTripStore((s) => s.loadTrip)
|
||||
const can = useCanDo()
|
||||
const canEditPlaces = can('place_edit', trip)
|
||||
|
||||
const handleGpxImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
@@ -46,7 +50,7 @@ const PlacesSidebar = React.memo(function PlacesSidebar({
|
||||
e.target.value = ''
|
||||
try {
|
||||
const result = await placesApi.importGpx(tripId, file)
|
||||
await tripStore.loadTrip(tripId)
|
||||
await loadTrip(tripId)
|
||||
toast.success(t('places.gpxImported', { count: result.count }))
|
||||
} catch (err: any) {
|
||||
toast.error(err?.response?.data?.error || t('places.gpxError'))
|
||||
@@ -88,7 +92,7 @@ const PlacesSidebar = React.memo(function PlacesSidebar({
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }}>
|
||||
{/* Kopfbereich */}
|
||||
<div style={{ padding: '14px 16px 10px', borderBottom: '1px solid var(--border-faint)', flexShrink: 0 }}>
|
||||
<button
|
||||
{canEditPlaces && <button
|
||||
onClick={onAddPlace}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
@@ -98,7 +102,8 @@ const PlacesSidebar = React.memo(function PlacesSidebar({
|
||||
}}
|
||||
>
|
||||
<Plus size={14} strokeWidth={2} /> {t('places.addPlace')}
|
||||
</button>
|
||||
</button>}
|
||||
{canEditPlaces && <>
|
||||
<input ref={gpxInputRef} type="file" accept=".gpx" style={{ display: 'none' }} onChange={handleGpxImport} />
|
||||
<button
|
||||
onClick={() => gpxInputRef.current?.click()}
|
||||
@@ -112,6 +117,7 @@ const PlacesSidebar = React.memo(function PlacesSidebar({
|
||||
>
|
||||
<Upload size={11} strokeWidth={2} /> {t('places.importGpx')}
|
||||
</button>
|
||||
</>}
|
||||
|
||||
{/* Filter-Tabs */}
|
||||
<div style={{ display: 'flex', gap: 4, marginBottom: 8 }}>
|
||||
@@ -252,12 +258,12 @@ const PlacesSidebar = React.memo(function PlacesSidebar({
|
||||
}
|
||||
}}
|
||||
onContextMenu={e => ctxMenu.open(e, [
|
||||
onEditPlace && { label: t('common.edit'), icon: Pencil, onClick: () => onEditPlace(place) },
|
||||
canEditPlaces && { label: t('common.edit'), icon: Pencil, onClick: () => onEditPlace(place) },
|
||||
selectedDayId && { label: t('planner.addToDay'), icon: CalendarDays, onClick: () => onAssignToDay(place.id, selectedDayId) },
|
||||
place.website && { label: t('inspector.website'), icon: ExternalLink, onClick: () => window.open(place.website, '_blank') },
|
||||
(place.lat && place.lng) && { label: 'Google Maps', icon: Navigation, onClick: () => window.open(`https://www.google.com/maps/search/?api=1&query=${place.lat},${place.lng}`, '_blank') },
|
||||
{ divider: true },
|
||||
onDeletePlace && { label: t('common.delete'), icon: Trash2, danger: true, onClick: () => onDeletePlace(place.id) },
|
||||
canEditPlaces && { label: t('common.delete'), icon: Trash2, danger: true, onClick: () => onDeletePlace(place.id) },
|
||||
])}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
|
||||
@@ -3,6 +3,8 @@ import Modal from '../shared/Modal'
|
||||
import { tripsApi, authApi, shareApi } from '../../api/client'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
import { useCanDo } from '../../store/permissionsStore'
|
||||
import { useTripStore } from '../../store/tripStore'
|
||||
import { Crown, UserMinus, UserPlus, Users, LogOut, Link2, Trash2, Copy, Check } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { getApiErrorMessage } from '../../types'
|
||||
@@ -32,7 +34,7 @@ function Avatar({ username, avatarUrl, size = 32 }: AvatarProps) {
|
||||
)
|
||||
}
|
||||
|
||||
function ShareLinkSection({ tripId, t }: { tripId: number; t: any }) {
|
||||
function ShareLinkSection({ tripId, t }: { tripId: number; t: (key: string, params?: Record<string, string | number>) => string }) {
|
||||
const [shareToken, setShareToken] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [copied, setCopied] = useState(false)
|
||||
@@ -172,6 +174,10 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle }:
|
||||
const toast = useToast()
|
||||
const { user } = useAuthStore()
|
||||
const { t } = useTranslation()
|
||||
const can = useCanDo()
|
||||
const trip = useTripStore((s) => s.trip)
|
||||
const canManageMembers = can('member_manage', trip)
|
||||
const canManageShare = can('share_manage', trip)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && tripId) {
|
||||
@@ -260,7 +266,7 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle }:
|
||||
</div>
|
||||
|
||||
{/* Add member dropdown */}
|
||||
<div>
|
||||
{canManageMembers && <div>
|
||||
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 8 }}>
|
||||
{t('members.inviteUser')}
|
||||
</label>
|
||||
@@ -293,10 +299,10 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle }:
|
||||
<UserPlus size={13} /> {adding ? '…' : t('members.invite')}
|
||||
</button>
|
||||
</div>
|
||||
{availableUsers.length === 0 && allUsers.length > 0 && (
|
||||
{availableUsers.length === 0 && allUsers.length > 0 && canManageMembers && (
|
||||
<p style={{ fontSize: 11.5, color: 'var(--text-faint)', margin: '6px 0 0' }}>{t('members.allHaveAccess')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{/* Members list */}
|
||||
<div>
|
||||
@@ -317,7 +323,7 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle }:
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{allMembers.map(member => {
|
||||
const isSelf = member.id === user?.id
|
||||
const canRemove = isCurrentOwner ? member.role !== 'owner' : isSelf
|
||||
const canRemove = isSelf || (canManageMembers && (isCurrentOwner ? member.role !== 'owner' : false))
|
||||
return (
|
||||
<div key={member.id} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
@@ -358,9 +364,9 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle }:
|
||||
</div>
|
||||
|
||||
{/* Right column: Share Link */}
|
||||
<div style={{ borderLeft: '1px solid var(--border-faint)', paddingLeft: 24 }}>
|
||||
{canManageShare && <div style={{ borderLeft: '1px solid var(--border-faint)', paddingLeft: 24 }}>
|
||||
<ShareLinkSection tripId={tripId} t={t} />
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<style>{`@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.5} }`}</style>
|
||||
</div>
|
||||
|
||||
@@ -1423,6 +1423,55 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'collab.polls.options': 'الخيارات',
|
||||
'collab.polls.delete': 'حذف',
|
||||
'collab.polls.closedSection': 'مغلق',
|
||||
|
||||
// Permissions
|
||||
'admin.tabs.permissions': 'الصلاحيات',
|
||||
'perm.title': 'إعدادات الصلاحيات',
|
||||
'perm.subtitle': 'التحكم في من يمكنه تنفيذ الإجراءات عبر التطبيق',
|
||||
'perm.saved': 'تم حفظ إعدادات الصلاحيات',
|
||||
'perm.resetDefaults': 'إعادة التعيين إلى الافتراضي',
|
||||
'perm.customized': 'مخصص',
|
||||
'perm.level.admin': 'المسؤول فقط',
|
||||
'perm.level.tripOwner': 'مالك الرحلة',
|
||||
'perm.level.tripMember': 'أعضاء الرحلة',
|
||||
'perm.level.everybody': 'الجميع',
|
||||
'perm.cat.trip': 'إدارة الرحلات',
|
||||
'perm.cat.members': 'إدارة الأعضاء',
|
||||
'perm.cat.files': 'الملفات',
|
||||
'perm.cat.content': 'المحتوى والجدول الزمني',
|
||||
'perm.cat.extras': 'الميزانية والتعبئة والتعاون',
|
||||
'perm.action.trip_create': 'إنشاء رحلات',
|
||||
'perm.action.trip_edit': 'تعديل تفاصيل الرحلة',
|
||||
'perm.action.trip_delete': 'حذف الرحلات',
|
||||
'perm.action.trip_archive': 'أرشفة / إلغاء أرشفة الرحلات',
|
||||
'perm.action.trip_cover_upload': 'رفع صورة الغلاف',
|
||||
'perm.action.member_manage': 'إضافة / إزالة الأعضاء',
|
||||
'perm.action.file_upload': 'رفع الملفات',
|
||||
'perm.action.file_edit': 'تعديل بيانات الملف',
|
||||
'perm.action.file_delete': 'حذف الملفات',
|
||||
'perm.action.place_edit': 'إضافة / تعديل / حذف الأماكن',
|
||||
'perm.action.day_edit': 'تعديل الأيام والملاحظات والتعيينات',
|
||||
'perm.action.reservation_edit': 'إدارة الحجوزات',
|
||||
'perm.action.budget_edit': 'إدارة الميزانية',
|
||||
'perm.action.packing_edit': 'إدارة قوائم التعبئة',
|
||||
'perm.action.collab_edit': 'التعاون (ملاحظات، استطلاعات، دردشة)',
|
||||
'perm.action.share_manage': 'إدارة روابط المشاركة',
|
||||
'perm.actionHint.trip_create': 'من يمكنه إنشاء رحلات جديدة',
|
||||
'perm.actionHint.trip_edit': 'من يمكنه تغيير اسم الرحلة والتواريخ والوصف والعملة',
|
||||
'perm.actionHint.trip_delete': 'من يمكنه حذف رحلة نهائياً',
|
||||
'perm.actionHint.trip_archive': 'من يمكنه أرشفة أو إلغاء أرشفة رحلة',
|
||||
'perm.actionHint.trip_cover_upload': 'من يمكنه رفع أو تغيير صورة الغلاف',
|
||||
'perm.actionHint.member_manage': 'من يمكنه دعوة أو إزالة أعضاء الرحلة',
|
||||
'perm.actionHint.file_upload': 'من يمكنه رفع ملفات إلى رحلة',
|
||||
'perm.actionHint.file_edit': 'من يمكنه تعديل أوصاف الملفات والروابط',
|
||||
'perm.actionHint.file_delete': 'من يمكنه نقل الملفات إلى سلة المهملات أو حذفها نهائياً',
|
||||
'perm.actionHint.place_edit': 'من يمكنه إضافة أو تعديل أو حذف الأماكن',
|
||||
'perm.actionHint.day_edit': 'من يمكنه تعديل الأيام وملاحظات الأيام وتعيينات الأماكن',
|
||||
'perm.actionHint.reservation_edit': 'من يمكنه إنشاء أو تعديل أو حذف الحجوزات',
|
||||
'perm.actionHint.budget_edit': 'من يمكنه إنشاء أو تعديل أو حذف عناصر الميزانية',
|
||||
'perm.actionHint.packing_edit': 'من يمكنه إدارة عناصر التعبئة والحقائب',
|
||||
'perm.actionHint.collab_edit': 'من يمكنه إنشاء ملاحظات واستطلاعات وإرسال رسائل',
|
||||
'perm.actionHint.share_manage': 'من يمكنه إنشاء أو حذف روابط المشاركة العامة',
|
||||
}
|
||||
|
||||
export default ar
|
||||
|
||||
@@ -1402,6 +1402,55 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.confirmShareTitle': 'Compartilhar com membros da viagem?',
|
||||
'memories.confirmShareHint': '{count} fotos serão visíveis para todos os membros desta viagem. Você pode tornar fotos individuais privadas depois.',
|
||||
'memories.confirmShareButton': 'Compartilhar fotos',
|
||||
|
||||
// Permissions
|
||||
'admin.tabs.permissions': 'Permissões',
|
||||
'perm.title': 'Configurações de Permissões',
|
||||
'perm.subtitle': 'Controle quem pode realizar ações no aplicativo',
|
||||
'perm.saved': 'Configurações de permissões salvas',
|
||||
'perm.resetDefaults': 'Restaurar padrões',
|
||||
'perm.customized': 'personalizado',
|
||||
'perm.level.admin': 'Apenas administrador',
|
||||
'perm.level.tripOwner': 'Dono da viagem',
|
||||
'perm.level.tripMember': 'Membros da viagem',
|
||||
'perm.level.everybody': 'Todos',
|
||||
'perm.cat.trip': 'Gerenciamento de Viagens',
|
||||
'perm.cat.members': 'Gerenciamento de Membros',
|
||||
'perm.cat.files': 'Arquivos',
|
||||
'perm.cat.content': 'Conteúdo e Cronograma',
|
||||
'perm.cat.extras': 'Orçamento, Bagagem e Colaboração',
|
||||
'perm.action.trip_create': 'Criar viagens',
|
||||
'perm.action.trip_edit': 'Editar detalhes da viagem',
|
||||
'perm.action.trip_delete': 'Excluir viagens',
|
||||
'perm.action.trip_archive': 'Arquivar / desarquivar viagens',
|
||||
'perm.action.trip_cover_upload': 'Enviar imagem de capa',
|
||||
'perm.action.member_manage': 'Adicionar / remover membros',
|
||||
'perm.action.file_upload': 'Enviar arquivos',
|
||||
'perm.action.file_edit': 'Editar metadados do arquivo',
|
||||
'perm.action.file_delete': 'Excluir arquivos',
|
||||
'perm.action.place_edit': 'Adicionar / editar / excluir lugares',
|
||||
'perm.action.day_edit': 'Editar dias, notas e atribuições',
|
||||
'perm.action.reservation_edit': 'Gerenciar reservas',
|
||||
'perm.action.budget_edit': 'Gerenciar orçamento',
|
||||
'perm.action.packing_edit': 'Gerenciar listas de bagagem',
|
||||
'perm.action.collab_edit': 'Colaboração (notas, enquetes, chat)',
|
||||
'perm.action.share_manage': 'Gerenciar links de compartilhamento',
|
||||
'perm.actionHint.trip_create': 'Quem pode criar novas viagens',
|
||||
'perm.actionHint.trip_edit': 'Quem pode alterar nome, datas, descrição e moeda da viagem',
|
||||
'perm.actionHint.trip_delete': 'Quem pode excluir permanentemente uma viagem',
|
||||
'perm.actionHint.trip_archive': 'Quem pode arquivar ou desarquivar uma viagem',
|
||||
'perm.actionHint.trip_cover_upload': 'Quem pode enviar ou alterar a imagem de capa',
|
||||
'perm.actionHint.member_manage': 'Quem pode convidar ou remover membros da viagem',
|
||||
'perm.actionHint.file_upload': 'Quem pode enviar arquivos para uma viagem',
|
||||
'perm.actionHint.file_edit': 'Quem pode editar descrições e links dos arquivos',
|
||||
'perm.actionHint.file_delete': 'Quem pode mover arquivos para a lixeira ou excluí-los permanentemente',
|
||||
'perm.actionHint.place_edit': 'Quem pode adicionar, editar ou excluir lugares',
|
||||
'perm.actionHint.day_edit': 'Quem pode editar dias, notas dos dias e atribuições de lugares',
|
||||
'perm.actionHint.reservation_edit': 'Quem pode criar, editar ou excluir reservas',
|
||||
'perm.actionHint.budget_edit': 'Quem pode criar, editar ou excluir itens do orçamento',
|
||||
'perm.actionHint.packing_edit': 'Quem pode gerenciar itens de bagagem e malas',
|
||||
'perm.actionHint.collab_edit': 'Quem pode criar notas, enquetes e enviar mensagens',
|
||||
'perm.actionHint.share_manage': 'Quem pode criar ou excluir links de compartilhamento públicos',
|
||||
}
|
||||
|
||||
export default br
|
||||
|
||||
@@ -1423,6 +1423,55 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'collab.polls.options': 'Možnosti',
|
||||
'collab.polls.delete': 'Smazat',
|
||||
'collab.polls.closedSection': 'Uzavřené',
|
||||
|
||||
// Permissions
|
||||
'admin.tabs.permissions': 'Oprávnění',
|
||||
'perm.title': 'Nastavení oprávnění',
|
||||
'perm.subtitle': 'Určete, kdo může provádět akce v aplikaci',
|
||||
'perm.saved': 'Nastavení oprávnění uloženo',
|
||||
'perm.resetDefaults': 'Obnovit výchozí',
|
||||
'perm.customized': 'upraveno',
|
||||
'perm.level.admin': 'Pouze administrátor',
|
||||
'perm.level.tripOwner': 'Vlastník výletu',
|
||||
'perm.level.tripMember': 'Členové výletu',
|
||||
'perm.level.everybody': 'Všichni',
|
||||
'perm.cat.trip': 'Správa výletů',
|
||||
'perm.cat.members': 'Správa členů',
|
||||
'perm.cat.files': 'Soubory',
|
||||
'perm.cat.content': 'Obsah a plán',
|
||||
'perm.cat.extras': 'Rozpočet, balení a spolupráce',
|
||||
'perm.action.trip_create': 'Vytvářet výlety',
|
||||
'perm.action.trip_edit': 'Upravit detaily výletu',
|
||||
'perm.action.trip_delete': 'Smazat výlety',
|
||||
'perm.action.trip_archive': 'Archivovat / odarchivovat výlety',
|
||||
'perm.action.trip_cover_upload': 'Nahrát titulní obrázek',
|
||||
'perm.action.member_manage': 'Přidat / odebrat členy',
|
||||
'perm.action.file_upload': 'Nahrát soubory',
|
||||
'perm.action.file_edit': 'Upravit metadata souborů',
|
||||
'perm.action.file_delete': 'Smazat soubory',
|
||||
'perm.action.place_edit': 'Přidat / upravit / smazat místa',
|
||||
'perm.action.day_edit': 'Upravit dny, poznámky a přiřazení',
|
||||
'perm.action.reservation_edit': 'Spravovat rezervace',
|
||||
'perm.action.budget_edit': 'Spravovat rozpočet',
|
||||
'perm.action.packing_edit': 'Spravovat seznamy balení',
|
||||
'perm.action.collab_edit': 'Spolupráce (poznámky, hlasování, chat)',
|
||||
'perm.action.share_manage': 'Spravovat odkazy ke sdílení',
|
||||
'perm.actionHint.trip_create': 'Kdo může vytvářet nové výlety',
|
||||
'perm.actionHint.trip_edit': 'Kdo může měnit název, data, popis a měnu výletu',
|
||||
'perm.actionHint.trip_delete': 'Kdo může trvale smazat výlet',
|
||||
'perm.actionHint.trip_archive': 'Kdo může archivovat nebo odarchivovat výlet',
|
||||
'perm.actionHint.trip_cover_upload': 'Kdo může nahrát nebo změnit titulní obrázek',
|
||||
'perm.actionHint.member_manage': 'Kdo může pozvat nebo odebrat členy výletu',
|
||||
'perm.actionHint.file_upload': 'Kdo může nahrávat soubory k výletu',
|
||||
'perm.actionHint.file_edit': 'Kdo může upravovat popisy a odkazy souborů',
|
||||
'perm.actionHint.file_delete': 'Kdo může přesunout soubory do koše nebo je trvale smazat',
|
||||
'perm.actionHint.place_edit': 'Kdo může přidávat, upravovat nebo mazat místa',
|
||||
'perm.actionHint.day_edit': 'Kdo může upravovat dny, poznámky ke dnům a přiřazení míst',
|
||||
'perm.actionHint.reservation_edit': 'Kdo může vytvářet, upravovat nebo mazat rezervace',
|
||||
'perm.actionHint.budget_edit': 'Kdo může vytvářet, upravovat nebo mazat položky rozpočtu',
|
||||
'perm.actionHint.packing_edit': 'Kdo může spravovat položky balení a tašky',
|
||||
'perm.actionHint.collab_edit': 'Kdo může vytvářet poznámky, hlasování a posílat zprávy',
|
||||
'perm.actionHint.share_manage': 'Kdo může vytvářet nebo mazat veřejné odkazy ke sdílení',
|
||||
}
|
||||
|
||||
export default cs
|
||||
|
||||
@@ -1420,6 +1420,55 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'collab.polls.options': 'Optionen',
|
||||
'collab.polls.delete': 'Löschen',
|
||||
'collab.polls.closedSection': 'Geschlossen',
|
||||
|
||||
// Permissions
|
||||
'admin.tabs.permissions': 'Berechtigungen',
|
||||
'perm.title': 'Berechtigungseinstellungen',
|
||||
'perm.subtitle': 'Steuern Sie, wer Aktionen in der Anwendung ausführen kann',
|
||||
'perm.saved': 'Berechtigungseinstellungen gespeichert',
|
||||
'perm.resetDefaults': 'Auf Standard zurücksetzen',
|
||||
'perm.customized': 'angepasst',
|
||||
'perm.level.admin': 'Nur Administrator',
|
||||
'perm.level.tripOwner': 'Reise-Eigentümer',
|
||||
'perm.level.tripMember': 'Reise-Mitglieder',
|
||||
'perm.level.everybody': 'Alle',
|
||||
'perm.cat.trip': 'Reiseverwaltung',
|
||||
'perm.cat.members': 'Mitgliederverwaltung',
|
||||
'perm.cat.files': 'Dateien',
|
||||
'perm.cat.content': 'Inhalte & Zeitplan',
|
||||
'perm.cat.extras': 'Budget, Packlisten & Zusammenarbeit',
|
||||
'perm.action.trip_create': 'Reisen erstellen',
|
||||
'perm.action.trip_edit': 'Reisedetails bearbeiten',
|
||||
'perm.action.trip_delete': 'Reisen löschen',
|
||||
'perm.action.trip_archive': 'Reisen archivieren / dearchivieren',
|
||||
'perm.action.trip_cover_upload': 'Titelbild hochladen',
|
||||
'perm.action.member_manage': 'Mitglieder hinzufügen / entfernen',
|
||||
'perm.action.file_upload': 'Dateien hochladen',
|
||||
'perm.action.file_edit': 'Datei-Metadaten bearbeiten',
|
||||
'perm.action.file_delete': 'Dateien löschen',
|
||||
'perm.action.place_edit': 'Orte hinzufügen / bearbeiten / löschen',
|
||||
'perm.action.day_edit': 'Tage, Notizen & Zuweisungen bearbeiten',
|
||||
'perm.action.reservation_edit': 'Reservierungen verwalten',
|
||||
'perm.action.budget_edit': 'Budget verwalten',
|
||||
'perm.action.packing_edit': 'Packlisten verwalten',
|
||||
'perm.action.collab_edit': 'Zusammenarbeit (Notizen, Umfragen, Chat)',
|
||||
'perm.action.share_manage': 'Freigabelinks verwalten',
|
||||
'perm.actionHint.trip_create': 'Wer kann neue Reisen erstellen',
|
||||
'perm.actionHint.trip_edit': 'Wer kann Reisename, Daten, Beschreibung und Währung ändern',
|
||||
'perm.actionHint.trip_delete': 'Wer kann eine Reise dauerhaft löschen',
|
||||
'perm.actionHint.trip_archive': 'Wer kann eine Reise archivieren oder dearchivieren',
|
||||
'perm.actionHint.trip_cover_upload': 'Wer kann das Titelbild hochladen oder ändern',
|
||||
'perm.actionHint.member_manage': 'Wer kann Reise-Mitglieder einladen oder entfernen',
|
||||
'perm.actionHint.file_upload': 'Wer kann Dateien zu einer Reise hochladen',
|
||||
'perm.actionHint.file_edit': 'Wer kann Dateibeschreibungen und Links bearbeiten',
|
||||
'perm.actionHint.file_delete': 'Wer kann Dateien in den Papierkorb verschieben oder dauerhaft löschen',
|
||||
'perm.actionHint.place_edit': 'Wer kann Orte hinzufügen, bearbeiten oder löschen',
|
||||
'perm.actionHint.day_edit': 'Wer kann Tage, Tagesnotizen und Ort-Zuweisungen bearbeiten',
|
||||
'perm.actionHint.reservation_edit': 'Wer kann Reservierungen erstellen, bearbeiten oder löschen',
|
||||
'perm.actionHint.budget_edit': 'Wer kann Budgetposten erstellen, bearbeiten oder löschen',
|
||||
'perm.actionHint.packing_edit': 'Wer kann Packstücke und Taschen verwalten',
|
||||
'perm.actionHint.collab_edit': 'Wer kann Notizen, Umfragen erstellen und Nachrichten senden',
|
||||
'perm.actionHint.share_manage': 'Wer kann öffentliche Freigabelinks erstellen oder löschen',
|
||||
}
|
||||
|
||||
export default de
|
||||
|
||||
@@ -1416,6 +1416,55 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'collab.polls.options': 'Options',
|
||||
'collab.polls.delete': 'Delete',
|
||||
'collab.polls.closedSection': 'Closed',
|
||||
|
||||
// Permissions
|
||||
'admin.tabs.permissions': 'Permissions',
|
||||
'perm.title': 'Permission Settings',
|
||||
'perm.subtitle': 'Control who can perform actions across the application',
|
||||
'perm.saved': 'Permission settings saved',
|
||||
'perm.resetDefaults': 'Reset to defaults',
|
||||
'perm.customized': 'customized',
|
||||
'perm.level.admin': 'Admin only',
|
||||
'perm.level.tripOwner': 'Trip owner',
|
||||
'perm.level.tripMember': 'Trip members',
|
||||
'perm.level.everybody': 'Everyone',
|
||||
'perm.cat.trip': 'Trip Management',
|
||||
'perm.cat.members': 'Member Management',
|
||||
'perm.cat.files': 'Files',
|
||||
'perm.cat.content': 'Content & Schedule',
|
||||
'perm.cat.extras': 'Budget, Packing & Collaboration',
|
||||
'perm.action.trip_create': 'Create trips',
|
||||
'perm.action.trip_edit': 'Edit trip details',
|
||||
'perm.action.trip_delete': 'Delete trips',
|
||||
'perm.action.trip_archive': 'Archive / unarchive trips',
|
||||
'perm.action.trip_cover_upload': 'Upload cover image',
|
||||
'perm.action.member_manage': 'Add / remove members',
|
||||
'perm.action.file_upload': 'Upload files',
|
||||
'perm.action.file_edit': 'Edit file metadata',
|
||||
'perm.action.file_delete': 'Delete files',
|
||||
'perm.action.place_edit': 'Add / edit / delete places',
|
||||
'perm.action.day_edit': 'Edit days, notes & assignments',
|
||||
'perm.action.reservation_edit': 'Manage reservations',
|
||||
'perm.action.budget_edit': 'Manage budget',
|
||||
'perm.action.packing_edit': 'Manage packing lists',
|
||||
'perm.action.collab_edit': 'Collaboration (notes, polls, chat)',
|
||||
'perm.action.share_manage': 'Manage share links',
|
||||
'perm.actionHint.trip_create': 'Who can create new trips',
|
||||
'perm.actionHint.trip_edit': 'Who can change trip name, dates, description and currency',
|
||||
'perm.actionHint.trip_delete': 'Who can permanently delete a trip',
|
||||
'perm.actionHint.trip_archive': 'Who can archive or unarchive a trip',
|
||||
'perm.actionHint.trip_cover_upload': 'Who can upload or change the cover image',
|
||||
'perm.actionHint.member_manage': 'Who can invite or remove trip members',
|
||||
'perm.actionHint.file_upload': 'Who can upload files to a trip',
|
||||
'perm.actionHint.file_edit': 'Who can edit file descriptions and links',
|
||||
'perm.actionHint.file_delete': 'Who can move files to trash or permanently delete them',
|
||||
'perm.actionHint.place_edit': 'Who can add, edit or delete places',
|
||||
'perm.actionHint.day_edit': 'Who can edit days, day notes and place assignments',
|
||||
'perm.actionHint.reservation_edit': 'Who can create, edit or delete reservations',
|
||||
'perm.actionHint.budget_edit': 'Who can create, edit or delete budget items',
|
||||
'perm.actionHint.packing_edit': 'Who can manage packing items and bags',
|
||||
'perm.actionHint.collab_edit': 'Who can create notes, polls and send messages',
|
||||
'perm.actionHint.share_manage': 'Who can create or delete public share links',
|
||||
}
|
||||
|
||||
export default en
|
||||
|
||||
@@ -1425,6 +1425,55 @@ const es: Record<string, string> = {
|
||||
// Settings (2.6.2)
|
||||
'settings.currentPasswordRequired': 'La contraseña actual es obligatoria',
|
||||
'settings.passwordWeak': 'La contraseña debe contener mayúsculas, minúsculas y números',
|
||||
|
||||
// Permissions
|
||||
'admin.tabs.permissions': 'Permisos',
|
||||
'perm.title': 'Configuración de permisos',
|
||||
'perm.subtitle': 'Controla quién puede realizar acciones en la aplicación',
|
||||
'perm.saved': 'Configuración de permisos guardada',
|
||||
'perm.resetDefaults': 'Restablecer valores predeterminados',
|
||||
'perm.customized': 'personalizado',
|
||||
'perm.level.admin': 'Solo administrador',
|
||||
'perm.level.tripOwner': 'Propietario del viaje',
|
||||
'perm.level.tripMember': 'Miembros del viaje',
|
||||
'perm.level.everybody': 'Todos',
|
||||
'perm.cat.trip': 'Gestión de viajes',
|
||||
'perm.cat.members': 'Gestión de miembros',
|
||||
'perm.cat.files': 'Archivos',
|
||||
'perm.cat.content': 'Contenido y horario',
|
||||
'perm.cat.extras': 'Presupuesto, equipaje y colaboración',
|
||||
'perm.action.trip_create': 'Crear viajes',
|
||||
'perm.action.trip_edit': 'Editar detalles del viaje',
|
||||
'perm.action.trip_delete': 'Eliminar viajes',
|
||||
'perm.action.trip_archive': 'Archivar / desarchivar viajes',
|
||||
'perm.action.trip_cover_upload': 'Subir imagen de portada',
|
||||
'perm.action.member_manage': 'Añadir / eliminar miembros',
|
||||
'perm.action.file_upload': 'Subir archivos',
|
||||
'perm.action.file_edit': 'Editar metadatos del archivo',
|
||||
'perm.action.file_delete': 'Eliminar archivos',
|
||||
'perm.action.place_edit': 'Añadir / editar / eliminar lugares',
|
||||
'perm.action.day_edit': 'Editar días, notas y asignaciones',
|
||||
'perm.action.reservation_edit': 'Gestionar reservas',
|
||||
'perm.action.budget_edit': 'Gestionar presupuesto',
|
||||
'perm.action.packing_edit': 'Gestionar listas de equipaje',
|
||||
'perm.action.collab_edit': 'Colaboración (notas, encuestas, chat)',
|
||||
'perm.action.share_manage': 'Gestionar enlaces compartidos',
|
||||
'perm.actionHint.trip_create': 'Quién puede crear nuevos viajes',
|
||||
'perm.actionHint.trip_edit': 'Quién puede cambiar el nombre, fechas, descripción y moneda del viaje',
|
||||
'perm.actionHint.trip_delete': 'Quién puede eliminar permanentemente un viaje',
|
||||
'perm.actionHint.trip_archive': 'Quién puede archivar o desarchivar un viaje',
|
||||
'perm.actionHint.trip_cover_upload': 'Quién puede subir o cambiar la imagen de portada',
|
||||
'perm.actionHint.member_manage': 'Quién puede invitar o eliminar miembros del viaje',
|
||||
'perm.actionHint.file_upload': 'Quién puede subir archivos a un viaje',
|
||||
'perm.actionHint.file_edit': 'Quién puede editar descripciones y enlaces de archivos',
|
||||
'perm.actionHint.file_delete': 'Quién puede mover archivos a la papelera o eliminarlos permanentemente',
|
||||
'perm.actionHint.place_edit': 'Quién puede añadir, editar o eliminar lugares',
|
||||
'perm.actionHint.day_edit': 'Quién puede editar días, notas de días y asignaciones de lugares',
|
||||
'perm.actionHint.reservation_edit': 'Quién puede crear, editar o eliminar reservas',
|
||||
'perm.actionHint.budget_edit': 'Quién puede crear, editar o eliminar partidas del presupuesto',
|
||||
'perm.actionHint.packing_edit': 'Quién puede gestionar artículos de equipaje y bolsas',
|
||||
'perm.actionHint.collab_edit': 'Quién puede crear notas, encuestas y enviar mensajes',
|
||||
'perm.actionHint.share_manage': 'Quién puede crear o eliminar enlaces compartidos públicos',
|
||||
}
|
||||
|
||||
export default es
|
||||
|
||||
@@ -1419,6 +1419,55 @@ const fr: Record<string, string> = {
|
||||
'collab.polls.options': 'Options',
|
||||
'collab.polls.delete': 'Supprimer',
|
||||
'collab.polls.closedSection': 'Fermés',
|
||||
|
||||
// Permissions
|
||||
'admin.tabs.permissions': 'Permissions',
|
||||
'perm.title': 'Paramètres des permissions',
|
||||
'perm.subtitle': 'Contrôlez qui peut effectuer des actions dans l\'application',
|
||||
'perm.saved': 'Paramètres des permissions enregistrés',
|
||||
'perm.resetDefaults': 'Réinitialiser par défaut',
|
||||
'perm.customized': 'personnalisé',
|
||||
'perm.level.admin': 'Administrateur uniquement',
|
||||
'perm.level.tripOwner': 'Propriétaire du voyage',
|
||||
'perm.level.tripMember': 'Membres du voyage',
|
||||
'perm.level.everybody': 'Tout le monde',
|
||||
'perm.cat.trip': 'Gestion des voyages',
|
||||
'perm.cat.members': 'Gestion des membres',
|
||||
'perm.cat.files': 'Fichiers',
|
||||
'perm.cat.content': 'Contenu et planning',
|
||||
'perm.cat.extras': 'Budget, bagages et collaboration',
|
||||
'perm.action.trip_create': 'Créer des voyages',
|
||||
'perm.action.trip_edit': 'Modifier les détails du voyage',
|
||||
'perm.action.trip_delete': 'Supprimer des voyages',
|
||||
'perm.action.trip_archive': 'Archiver / désarchiver des voyages',
|
||||
'perm.action.trip_cover_upload': 'Télécharger l\'image de couverture',
|
||||
'perm.action.member_manage': 'Ajouter / supprimer des membres',
|
||||
'perm.action.file_upload': 'Télécharger des fichiers',
|
||||
'perm.action.file_edit': 'Modifier les métadonnées des fichiers',
|
||||
'perm.action.file_delete': 'Supprimer des fichiers',
|
||||
'perm.action.place_edit': 'Ajouter / modifier / supprimer des lieux',
|
||||
'perm.action.day_edit': 'Modifier les jours, notes et affectations',
|
||||
'perm.action.reservation_edit': 'Gérer les réservations',
|
||||
'perm.action.budget_edit': 'Gérer le budget',
|
||||
'perm.action.packing_edit': 'Gérer les listes de bagages',
|
||||
'perm.action.collab_edit': 'Collaboration (notes, sondages, chat)',
|
||||
'perm.action.share_manage': 'Gérer les liens de partage',
|
||||
'perm.actionHint.trip_create': 'Qui peut créer de nouveaux voyages',
|
||||
'perm.actionHint.trip_edit': 'Qui peut modifier le nom, les dates, la description et la devise du voyage',
|
||||
'perm.actionHint.trip_delete': 'Qui peut supprimer définitivement un voyage',
|
||||
'perm.actionHint.trip_archive': 'Qui peut archiver ou désarchiver un voyage',
|
||||
'perm.actionHint.trip_cover_upload': 'Qui peut télécharger ou modifier l\'image de couverture',
|
||||
'perm.actionHint.member_manage': 'Qui peut inviter ou supprimer des membres du voyage',
|
||||
'perm.actionHint.file_upload': 'Qui peut télécharger des fichiers vers un voyage',
|
||||
'perm.actionHint.file_edit': 'Qui peut modifier les descriptions et liens des fichiers',
|
||||
'perm.actionHint.file_delete': 'Qui peut déplacer des fichiers vers la corbeille ou les supprimer définitivement',
|
||||
'perm.actionHint.place_edit': 'Qui peut ajouter, modifier ou supprimer des lieux',
|
||||
'perm.actionHint.day_edit': 'Qui peut modifier les jours, notes de jours et affectations de lieux',
|
||||
'perm.actionHint.reservation_edit': 'Qui peut créer, modifier ou supprimer des réservations',
|
||||
'perm.actionHint.budget_edit': 'Qui peut créer, modifier ou supprimer des éléments de budget',
|
||||
'perm.actionHint.packing_edit': 'Qui peut gérer les articles de bagages et les sacs',
|
||||
'perm.actionHint.collab_edit': 'Qui peut créer des notes, des sondages et envoyer des messages',
|
||||
'perm.actionHint.share_manage': 'Qui peut créer ou supprimer des liens de partage publics',
|
||||
}
|
||||
|
||||
export default fr
|
||||
|
||||
@@ -1418,6 +1418,55 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.confirmShareTitle': 'Megosztás az utazótársakkal?',
|
||||
'memories.confirmShareHint': '{count} fotó lesz látható az utazás összes tagja számára. Később egyenként is priváttá teheted őket.',
|
||||
'memories.confirmShareButton': 'Fotók megosztása',
|
||||
|
||||
// Permissions
|
||||
'admin.tabs.permissions': 'Jogosultságok',
|
||||
'perm.title': 'Jogosultsági beállítások',
|
||||
'perm.subtitle': 'Szabályozd, ki milyen műveleteket végezhet az alkalmazásban',
|
||||
'perm.saved': 'Jogosultsági beállítások mentve',
|
||||
'perm.resetDefaults': 'Alapértelmezések visszaállítása',
|
||||
'perm.customized': 'testreszabott',
|
||||
'perm.level.admin': 'Csak adminisztrátor',
|
||||
'perm.level.tripOwner': 'Utazás tulajdonosa',
|
||||
'perm.level.tripMember': 'Utazás tagjai',
|
||||
'perm.level.everybody': 'Mindenki',
|
||||
'perm.cat.trip': 'Utazáskezelés',
|
||||
'perm.cat.members': 'Tagkezelés',
|
||||
'perm.cat.files': 'Fájlok',
|
||||
'perm.cat.content': 'Tartalom és menetrend',
|
||||
'perm.cat.extras': 'Költségvetés, csomagolás és együttműködés',
|
||||
'perm.action.trip_create': 'Utazások létrehozása',
|
||||
'perm.action.trip_edit': 'Utazás részleteinek szerkesztése',
|
||||
'perm.action.trip_delete': 'Utazások törlése',
|
||||
'perm.action.trip_archive': 'Utazások archiválása / visszaállítása',
|
||||
'perm.action.trip_cover_upload': 'Borítókép feltöltése',
|
||||
'perm.action.member_manage': 'Tagok hozzáadása / eltávolítása',
|
||||
'perm.action.file_upload': 'Fájlok feltöltése',
|
||||
'perm.action.file_edit': 'Fájl metaadatok szerkesztése',
|
||||
'perm.action.file_delete': 'Fájlok törlése',
|
||||
'perm.action.place_edit': 'Helyek hozzáadása / szerkesztése / törlése',
|
||||
'perm.action.day_edit': 'Napok, jegyzetek és hozzárendelések szerkesztése',
|
||||
'perm.action.reservation_edit': 'Foglalások kezelése',
|
||||
'perm.action.budget_edit': 'Költségvetés kezelése',
|
||||
'perm.action.packing_edit': 'Csomagolási listák kezelése',
|
||||
'perm.action.collab_edit': 'Együttműködés (jegyzetek, szavazások, chat)',
|
||||
'perm.action.share_manage': 'Megosztási linkek kezelése',
|
||||
'perm.actionHint.trip_create': 'Ki hozhat létre új utazásokat',
|
||||
'perm.actionHint.trip_edit': 'Ki módosíthatja az utazás nevét, dátumait, leírását és pénznemét',
|
||||
'perm.actionHint.trip_delete': 'Ki törölhet véglegesen egy utazást',
|
||||
'perm.actionHint.trip_archive': 'Ki archiválhat vagy állíthat vissza egy utazást',
|
||||
'perm.actionHint.trip_cover_upload': 'Ki tölthet fel vagy módosíthat borítóképet',
|
||||
'perm.actionHint.member_manage': 'Ki hívhat meg vagy távolíthat el utazás tagokat',
|
||||
'perm.actionHint.file_upload': 'Ki tölthet fel fájlokat egy utazáshoz',
|
||||
'perm.actionHint.file_edit': 'Ki szerkesztheti a fájlok leírásait és linkjeit',
|
||||
'perm.actionHint.file_delete': 'Ki helyezhet fájlokat a kukába vagy törölheti véglegesen',
|
||||
'perm.actionHint.place_edit': 'Ki adhat hozzá, szerkeszthet vagy törölhet helyeket',
|
||||
'perm.actionHint.day_edit': 'Ki szerkesztheti a napokat, napi jegyzeteket és hely-hozzárendeléseket',
|
||||
'perm.actionHint.reservation_edit': 'Ki hozhat létre, szerkeszthet vagy törölhet foglalásokat',
|
||||
'perm.actionHint.budget_edit': 'Ki hozhat létre, szerkeszthet vagy törölhet költségvetési tételeket',
|
||||
'perm.actionHint.packing_edit': 'Ki kezelheti a csomagolási tételeket és táskákat',
|
||||
'perm.actionHint.collab_edit': 'Ki hozhat létre jegyzeteket, szavazásokat és küldhet üzeneteket',
|
||||
'perm.actionHint.share_manage': 'Ki hozhat létre vagy törölhet nyilvános megosztási linkeket',
|
||||
}
|
||||
|
||||
export default hu
|
||||
|
||||
@@ -1418,6 +1418,55 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'collab.polls.options': 'Opzioni',
|
||||
'collab.polls.delete': 'Elimina',
|
||||
'collab.polls.closedSection': 'Chiusi',
|
||||
|
||||
// Permissions
|
||||
'admin.tabs.permissions': 'Permessi',
|
||||
'perm.title': 'Impostazioni dei permessi',
|
||||
'perm.subtitle': 'Controlla chi può eseguire azioni nell\'applicazione',
|
||||
'perm.saved': 'Impostazioni dei permessi salvate',
|
||||
'perm.resetDefaults': 'Ripristina predefiniti',
|
||||
'perm.customized': 'personalizzato',
|
||||
'perm.level.admin': 'Solo amministratore',
|
||||
'perm.level.tripOwner': 'Proprietario del viaggio',
|
||||
'perm.level.tripMember': 'Membri del viaggio',
|
||||
'perm.level.everybody': 'Tutti',
|
||||
'perm.cat.trip': 'Gestione viaggi',
|
||||
'perm.cat.members': 'Gestione membri',
|
||||
'perm.cat.files': 'File',
|
||||
'perm.cat.content': 'Contenuti e programma',
|
||||
'perm.cat.extras': 'Budget, bagagli e collaborazione',
|
||||
'perm.action.trip_create': 'Creare viaggi',
|
||||
'perm.action.trip_edit': 'Modificare dettagli del viaggio',
|
||||
'perm.action.trip_delete': 'Eliminare viaggi',
|
||||
'perm.action.trip_archive': 'Archiviare / dearchiviare viaggi',
|
||||
'perm.action.trip_cover_upload': 'Caricare immagine di copertina',
|
||||
'perm.action.member_manage': 'Aggiungere / rimuovere membri',
|
||||
'perm.action.file_upload': 'Caricare file',
|
||||
'perm.action.file_edit': 'Modificare metadati dei file',
|
||||
'perm.action.file_delete': 'Eliminare file',
|
||||
'perm.action.place_edit': 'Aggiungere / modificare / eliminare luoghi',
|
||||
'perm.action.day_edit': 'Modificare giorni, note e assegnazioni',
|
||||
'perm.action.reservation_edit': 'Gestire prenotazioni',
|
||||
'perm.action.budget_edit': 'Gestire budget',
|
||||
'perm.action.packing_edit': 'Gestire liste bagagli',
|
||||
'perm.action.collab_edit': 'Collaborazione (note, sondaggi, chat)',
|
||||
'perm.action.share_manage': 'Gestire link di condivisione',
|
||||
'perm.actionHint.trip_create': 'Chi può creare nuovi viaggi',
|
||||
'perm.actionHint.trip_edit': 'Chi può modificare nome, date, descrizione e valuta del viaggio',
|
||||
'perm.actionHint.trip_delete': 'Chi può eliminare definitivamente un viaggio',
|
||||
'perm.actionHint.trip_archive': 'Chi può archiviare o dearchiviare un viaggio',
|
||||
'perm.actionHint.trip_cover_upload': 'Chi può caricare o modificare l\'immagine di copertina',
|
||||
'perm.actionHint.member_manage': 'Chi può invitare o rimuovere membri del viaggio',
|
||||
'perm.actionHint.file_upload': 'Chi può caricare file in un viaggio',
|
||||
'perm.actionHint.file_edit': 'Chi può modificare descrizioni e link dei file',
|
||||
'perm.actionHint.file_delete': 'Chi può spostare file nel cestino o eliminarli definitivamente',
|
||||
'perm.actionHint.place_edit': 'Chi può aggiungere, modificare o eliminare luoghi',
|
||||
'perm.actionHint.day_edit': 'Chi può modificare giorni, note dei giorni e assegnazioni dei luoghi',
|
||||
'perm.actionHint.reservation_edit': 'Chi può creare, modificare o eliminare prenotazioni',
|
||||
'perm.actionHint.budget_edit': 'Chi può creare, modificare o eliminare voci di budget',
|
||||
'perm.actionHint.packing_edit': 'Chi può gestire articoli da bagaglio e borse',
|
||||
'perm.actionHint.collab_edit': 'Chi può creare note, sondaggi e inviare messaggi',
|
||||
'perm.actionHint.share_manage': 'Chi può creare o eliminare link di condivisione pubblici',
|
||||
}
|
||||
|
||||
export default it
|
||||
@@ -1419,6 +1419,55 @@ const nl: Record<string, string> = {
|
||||
'collab.polls.options': 'Opties',
|
||||
'collab.polls.delete': 'Verwijderen',
|
||||
'collab.polls.closedSection': 'Gesloten',
|
||||
|
||||
// Permissions
|
||||
'admin.tabs.permissions': 'Rechten',
|
||||
'perm.title': 'Rechtinstellingen',
|
||||
'perm.subtitle': 'Bepaal wie welke acties mag uitvoeren in de applicatie',
|
||||
'perm.saved': 'Rechtinstellingen opgeslagen',
|
||||
'perm.resetDefaults': 'Standaardwaarden herstellen',
|
||||
'perm.customized': 'aangepast',
|
||||
'perm.level.admin': 'Alleen beheerder',
|
||||
'perm.level.tripOwner': 'Reiseigenaar',
|
||||
'perm.level.tripMember': 'Reisleden',
|
||||
'perm.level.everybody': 'Iedereen',
|
||||
'perm.cat.trip': 'Reisbeheer',
|
||||
'perm.cat.members': 'Ledenbeheer',
|
||||
'perm.cat.files': 'Bestanden',
|
||||
'perm.cat.content': 'Inhoud & planning',
|
||||
'perm.cat.extras': 'Budget, paklijsten & samenwerking',
|
||||
'perm.action.trip_create': 'Reizen aanmaken',
|
||||
'perm.action.trip_edit': 'Reisdetails bewerken',
|
||||
'perm.action.trip_delete': 'Reizen verwijderen',
|
||||
'perm.action.trip_archive': 'Reizen archiveren / dearchiveren',
|
||||
'perm.action.trip_cover_upload': 'Omslagfoto uploaden',
|
||||
'perm.action.member_manage': 'Leden toevoegen / verwijderen',
|
||||
'perm.action.file_upload': 'Bestanden uploaden',
|
||||
'perm.action.file_edit': 'Bestandsmetadata bewerken',
|
||||
'perm.action.file_delete': 'Bestanden verwijderen',
|
||||
'perm.action.place_edit': 'Plaatsen toevoegen / bewerken / verwijderen',
|
||||
'perm.action.day_edit': 'Dagen, notities & toewijzingen bewerken',
|
||||
'perm.action.reservation_edit': 'Reserveringen beheren',
|
||||
'perm.action.budget_edit': 'Budget beheren',
|
||||
'perm.action.packing_edit': 'Paklijsten beheren',
|
||||
'perm.action.collab_edit': 'Samenwerking (notities, polls, chat)',
|
||||
'perm.action.share_manage': 'Deellinks beheren',
|
||||
'perm.actionHint.trip_create': 'Wie kan nieuwe reizen aanmaken',
|
||||
'perm.actionHint.trip_edit': 'Wie kan reisnaam, data, beschrijving en valuta wijzigen',
|
||||
'perm.actionHint.trip_delete': 'Wie kan een reis permanent verwijderen',
|
||||
'perm.actionHint.trip_archive': 'Wie kan een reis archiveren of dearchiveren',
|
||||
'perm.actionHint.trip_cover_upload': 'Wie kan de omslagfoto uploaden of wijzigen',
|
||||
'perm.actionHint.member_manage': 'Wie kan reisleden uitnodigen of verwijderen',
|
||||
'perm.actionHint.file_upload': 'Wie kan bestanden uploaden naar een reis',
|
||||
'perm.actionHint.file_edit': 'Wie kan bestandsbeschrijvingen en links bewerken',
|
||||
'perm.actionHint.file_delete': 'Wie kan bestanden naar de prullenbak verplaatsen of permanent verwijderen',
|
||||
'perm.actionHint.place_edit': 'Wie kan plaatsen toevoegen, bewerken of verwijderen',
|
||||
'perm.actionHint.day_edit': 'Wie kan dagen, dagnotities en plaatstoewijzingen bewerken',
|
||||
'perm.actionHint.reservation_edit': 'Wie kan reserveringen aanmaken, bewerken of verwijderen',
|
||||
'perm.actionHint.budget_edit': 'Wie kan budgetposten aanmaken, bewerken of verwijderen',
|
||||
'perm.actionHint.packing_edit': 'Wie kan pakitems en tassen beheren',
|
||||
'perm.actionHint.collab_edit': 'Wie kan notities, polls aanmaken en berichten versturen',
|
||||
'perm.actionHint.share_manage': 'Wie kan openbare deellinks aanmaken of verwijderen',
|
||||
}
|
||||
|
||||
export default nl
|
||||
|
||||
@@ -1419,6 +1419,55 @@ const ru: Record<string, string> = {
|
||||
'collab.polls.options': 'Варианты',
|
||||
'collab.polls.delete': 'Удалить',
|
||||
'collab.polls.closedSection': 'Закрытые',
|
||||
|
||||
// Permissions
|
||||
'admin.tabs.permissions': 'Разрешения',
|
||||
'perm.title': 'Настройки разрешений',
|
||||
'perm.subtitle': 'Управляйте тем, кто может выполнять действия в приложении',
|
||||
'perm.saved': 'Настройки разрешений сохранены',
|
||||
'perm.resetDefaults': 'Сбросить по умолчанию',
|
||||
'perm.customized': 'изменено',
|
||||
'perm.level.admin': 'Только администратор',
|
||||
'perm.level.tripOwner': 'Владелец поездки',
|
||||
'perm.level.tripMember': 'Участники поездки',
|
||||
'perm.level.everybody': 'Все',
|
||||
'perm.cat.trip': 'Управление поездками',
|
||||
'perm.cat.members': 'Управление участниками',
|
||||
'perm.cat.files': 'Файлы',
|
||||
'perm.cat.content': 'Контент и расписание',
|
||||
'perm.cat.extras': 'Бюджет, сборы и совместная работа',
|
||||
'perm.action.trip_create': 'Создавать поездки',
|
||||
'perm.action.trip_edit': 'Редактировать детали поездки',
|
||||
'perm.action.trip_delete': 'Удалять поездки',
|
||||
'perm.action.trip_archive': 'Архивировать / разархивировать поездки',
|
||||
'perm.action.trip_cover_upload': 'Загружать обложку',
|
||||
'perm.action.member_manage': 'Добавлять / удалять участников',
|
||||
'perm.action.file_upload': 'Загружать файлы',
|
||||
'perm.action.file_edit': 'Редактировать метаданные файлов',
|
||||
'perm.action.file_delete': 'Удалять файлы',
|
||||
'perm.action.place_edit': 'Добавлять / редактировать / удалять места',
|
||||
'perm.action.day_edit': 'Редактировать дни, заметки и назначения',
|
||||
'perm.action.reservation_edit': 'Управлять бронированиями',
|
||||
'perm.action.budget_edit': 'Управлять бюджетом',
|
||||
'perm.action.packing_edit': 'Управлять списками вещей',
|
||||
'perm.action.collab_edit': 'Совместная работа (заметки, опросы, чат)',
|
||||
'perm.action.share_manage': 'Управлять ссылками для обмена',
|
||||
'perm.actionHint.trip_create': 'Кто может создавать новые поездки',
|
||||
'perm.actionHint.trip_edit': 'Кто может менять название, даты, описание и валюту поездки',
|
||||
'perm.actionHint.trip_delete': 'Кто может безвозвратно удалить поездку',
|
||||
'perm.actionHint.trip_archive': 'Кто может архивировать или разархивировать поездку',
|
||||
'perm.actionHint.trip_cover_upload': 'Кто может загружать или менять обложку',
|
||||
'perm.actionHint.member_manage': 'Кто может приглашать или удалять участников поездки',
|
||||
'perm.actionHint.file_upload': 'Кто может загружать файлы в поездку',
|
||||
'perm.actionHint.file_edit': 'Кто может редактировать описания и ссылки файлов',
|
||||
'perm.actionHint.file_delete': 'Кто может перемещать файлы в корзину или безвозвратно удалять',
|
||||
'perm.actionHint.place_edit': 'Кто может добавлять, редактировать или удалять места',
|
||||
'perm.actionHint.day_edit': 'Кто может редактировать дни, заметки к дням и назначения мест',
|
||||
'perm.actionHint.reservation_edit': 'Кто может создавать, редактировать или удалять бронирования',
|
||||
'perm.actionHint.budget_edit': 'Кто может создавать, редактировать или удалять статьи бюджета',
|
||||
'perm.actionHint.packing_edit': 'Кто может управлять вещами для сборов и сумками',
|
||||
'perm.actionHint.collab_edit': 'Кто может создавать заметки, опросы и отправлять сообщения',
|
||||
'perm.actionHint.share_manage': 'Кто может создавать или удалять публичные ссылки для обмена',
|
||||
}
|
||||
|
||||
export default ru
|
||||
|
||||
@@ -1419,6 +1419,55 @@ const zh: Record<string, string> = {
|
||||
'collab.polls.options': '选项',
|
||||
'collab.polls.delete': '删除',
|
||||
'collab.polls.closedSection': '已关闭',
|
||||
|
||||
// Permissions
|
||||
'admin.tabs.permissions': '权限',
|
||||
'perm.title': '权限设置',
|
||||
'perm.subtitle': '控制谁可以在应用中执行操作',
|
||||
'perm.saved': '权限设置已保存',
|
||||
'perm.resetDefaults': '恢复默认',
|
||||
'perm.customized': '已自定义',
|
||||
'perm.level.admin': '仅管理员',
|
||||
'perm.level.tripOwner': '旅行所有者',
|
||||
'perm.level.tripMember': '旅行成员',
|
||||
'perm.level.everybody': '所有人',
|
||||
'perm.cat.trip': '旅行管理',
|
||||
'perm.cat.members': '成员管理',
|
||||
'perm.cat.files': '文件',
|
||||
'perm.cat.content': '内容与日程',
|
||||
'perm.cat.extras': '预算、行李与协作',
|
||||
'perm.action.trip_create': '创建旅行',
|
||||
'perm.action.trip_edit': '编辑旅行详情',
|
||||
'perm.action.trip_delete': '删除旅行',
|
||||
'perm.action.trip_archive': '归档 / 取消归档旅行',
|
||||
'perm.action.trip_cover_upload': '上传封面图片',
|
||||
'perm.action.member_manage': '添加 / 移除成员',
|
||||
'perm.action.file_upload': '上传文件',
|
||||
'perm.action.file_edit': '编辑文件元数据',
|
||||
'perm.action.file_delete': '删除文件',
|
||||
'perm.action.place_edit': '添加 / 编辑 / 删除地点',
|
||||
'perm.action.day_edit': '编辑日程、备注与分配',
|
||||
'perm.action.reservation_edit': '管理预订',
|
||||
'perm.action.budget_edit': '管理预算',
|
||||
'perm.action.packing_edit': '管理行李清单',
|
||||
'perm.action.collab_edit': '协作(笔记、投票、聊天)',
|
||||
'perm.action.share_manage': '管理分享链接',
|
||||
'perm.actionHint.trip_create': '谁可以创建新旅行',
|
||||
'perm.actionHint.trip_edit': '谁可以更改旅行名称、日期、描述和货币',
|
||||
'perm.actionHint.trip_delete': '谁可以永久删除旅行',
|
||||
'perm.actionHint.trip_archive': '谁可以归档或取消归档旅行',
|
||||
'perm.actionHint.trip_cover_upload': '谁可以上传或更改封面图片',
|
||||
'perm.actionHint.member_manage': '谁可以邀请或移除旅行成员',
|
||||
'perm.actionHint.file_upload': '谁可以向旅行上传文件',
|
||||
'perm.actionHint.file_edit': '谁可以编辑文件描述和链接',
|
||||
'perm.actionHint.file_delete': '谁可以将文件移至回收站或永久删除',
|
||||
'perm.actionHint.place_edit': '谁可以添加、编辑或删除地点',
|
||||
'perm.actionHint.day_edit': '谁可以编辑日程、日程备注和地点分配',
|
||||
'perm.actionHint.reservation_edit': '谁可以创建、编辑或删除预订',
|
||||
'perm.actionHint.budget_edit': '谁可以创建、编辑或删除预算项目',
|
||||
'perm.actionHint.packing_edit': '谁可以管理行李物品和包袋',
|
||||
'perm.actionHint.collab_edit': '谁可以创建笔记、投票和发送消息',
|
||||
'perm.actionHint.share_manage': '谁可以创建或删除公开分享链接',
|
||||
}
|
||||
|
||||
export default zh
|
||||
|
||||
@@ -15,6 +15,7 @@ import AddonManager from '../components/Admin/AddonManager'
|
||||
import PackingTemplateManager from '../components/Admin/PackingTemplateManager'
|
||||
import AuditLogPanel from '../components/Admin/AuditLogPanel'
|
||||
import AdminMcpTokensPanel from '../components/Admin/AdminMcpTokensPanel'
|
||||
import PermissionsPanel from '../components/Admin/PermissionsPanel'
|
||||
import { Users, Map, Briefcase, Shield, Trash2, Edit2, Camera, FileText, Eye, EyeOff, Save, CheckCircle, XCircle, Loader2, UserPlus, ArrowUpCircle, ExternalLink, Download, AlertTriangle, RefreshCw, GitBranch, Sun, Link2, Copy, Plus } from 'lucide-react'
|
||||
import CustomSelect from '../components/shared/CustomSelect'
|
||||
|
||||
@@ -61,6 +62,7 @@ export default function AdminPage(): React.ReactElement {
|
||||
{ id: 'users', label: t('admin.tabs.users') },
|
||||
{ id: 'config', label: t('admin.tabs.config') },
|
||||
{ id: 'addons', label: t('admin.tabs.addons') },
|
||||
{ id: 'permissions', label: t('admin.tabs.permissions') },
|
||||
{ id: 'settings', label: t('admin.tabs.settings') },
|
||||
{ id: 'backup', label: t('admin.tabs.backup') },
|
||||
{ id: 'audit', label: t('admin.tabs.audit') },
|
||||
@@ -1153,6 +1155,8 @@ export default function AdminPage(): React.ReactElement {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'permissions' && <PermissionsPanel />}
|
||||
|
||||
{activeTab === 'backup' && <BackupPanel />}
|
||||
|
||||
{activeTab === 'audit' && <AuditLogPanel serverTimezone={serverTimezone} />}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Archive, ArchiveRestore, Clock, MapPin, Settings, X, ArrowRightLeft,
|
||||
LayoutGrid, List,
|
||||
} from 'lucide-react'
|
||||
import { useCanDo } from '../store/permissionsStore'
|
||||
|
||||
interface DashboardTrip {
|
||||
id: number
|
||||
@@ -139,9 +140,9 @@ function LiquidGlass({ children, dark, style, className = '', onClick }: LiquidG
|
||||
// ── Spotlight Card (next upcoming trip) ─────────────────────────────────────
|
||||
interface TripCardProps {
|
||||
trip: DashboardTrip
|
||||
onEdit: (trip: DashboardTrip) => void
|
||||
onDelete: (trip: DashboardTrip) => void
|
||||
onArchive: (id: number) => void
|
||||
onEdit?: (trip: DashboardTrip) => void
|
||||
onDelete?: (trip: DashboardTrip) => void
|
||||
onArchive?: (id: number) => void
|
||||
onClick: (trip: DashboardTrip) => void
|
||||
t: (key: string, params?: Record<string, string | number | null>) => string
|
||||
locale: string
|
||||
@@ -188,12 +189,12 @@ function SpotlightCard({ trip, onEdit, onDelete, onArchive, onClick, t, locale,
|
||||
</div>
|
||||
|
||||
{/* Top-right actions */}
|
||||
{(!!trip.is_owner || isAdmin) && (
|
||||
{(onEdit || onArchive || onDelete) && (
|
||||
<div style={{ position: 'absolute', top: 16, right: 16, display: 'flex', gap: 6 }}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<IconBtn onClick={() => onEdit(trip)} title={t('common.edit')}><Edit2 size={14} /></IconBtn>
|
||||
<IconBtn onClick={() => onArchive(trip.id)} title={t('dashboard.archive')}><Archive size={14} /></IconBtn>
|
||||
<IconBtn onClick={() => onDelete(trip)} title={t('common.delete')} danger><Trash2 size={14} /></IconBtn>
|
||||
{onEdit && <IconBtn onClick={() => onEdit(trip)} title={t('common.edit')}><Edit2 size={14} /></IconBtn>}
|
||||
{onArchive && <IconBtn onClick={() => onArchive(trip.id)} title={t('dashboard.archive')}><Archive size={14} /></IconBtn>}
|
||||
{onDelete && <IconBtn onClick={() => onDelete(trip)} title={t('common.delete')} danger><Trash2 size={14} /></IconBtn>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -309,12 +310,12 @@ function TripCard({ trip, onEdit, onDelete, onArchive, onClick, t, locale, isAdm
|
||||
<Stat label={t('dashboard.places')} value={trip.place_count || 0} />
|
||||
</div>
|
||||
|
||||
{(!!trip.is_owner || isAdmin) && (
|
||||
{(onEdit || onArchive || onDelete) && (
|
||||
<div style={{ display: 'flex', gap: 6, borderTop: '1px solid #f3f4f6', paddingTop: 10 }}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<CardAction onClick={() => onEdit(trip)} icon={<Edit2 size={12} />} label={t('common.edit')} />
|
||||
<CardAction onClick={() => onArchive(trip.id)} icon={<Archive size={12} />} label={t('dashboard.archive')} />
|
||||
<CardAction onClick={() => onDelete(trip)} icon={<Trash2 size={12} />} label={t('common.delete')} danger />
|
||||
{onEdit && <CardAction onClick={() => onEdit(trip)} icon={<Edit2 size={12} />} label={t('common.edit')} />}
|
||||
{onArchive && <CardAction onClick={() => onArchive(trip.id)} icon={<Archive size={12} />} label={t('dashboard.archive')} />}
|
||||
{onDelete && <CardAction onClick={() => onDelete(trip)} icon={<Trash2 size={12} />} label={t('common.delete')} danger />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -411,9 +412,9 @@ function TripListItem({ trip, onEdit, onDelete, onArchive, onClick, t, locale, i
|
||||
{/* Actions */}
|
||||
{(!!trip.is_owner || isAdmin) && (
|
||||
<div style={{ display: 'flex', gap: 4, flexShrink: 0 }} onClick={e => e.stopPropagation()}>
|
||||
<CardAction onClick={() => onEdit(trip)} icon={<Edit2 size={12} />} label="" />
|
||||
<CardAction onClick={() => onArchive(trip.id)} icon={<Archive size={12} />} label="" />
|
||||
<CardAction onClick={() => onDelete(trip)} icon={<Trash2 size={12} />} label="" danger />
|
||||
{onEdit && <CardAction onClick={() => onEdit(trip)} icon={<Edit2 size={12} />} label="" />}
|
||||
{onArchive && <CardAction onClick={() => onArchive(trip.id)} icon={<Archive size={12} />} label="" />}
|
||||
{onDelete && <CardAction onClick={() => onDelete(trip)} icon={<Trash2 size={12} />} label="" danger />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -423,9 +424,9 @@ function TripListItem({ trip, onEdit, onDelete, onArchive, onClick, t, locale, i
|
||||
// ── Archived Trip Row ────────────────────────────────────────────────────────
|
||||
interface ArchivedRowProps {
|
||||
trip: DashboardTrip
|
||||
onEdit: (trip: DashboardTrip) => void
|
||||
onUnarchive: (id: number) => void
|
||||
onDelete: (trip: DashboardTrip) => void
|
||||
onEdit?: (trip: DashboardTrip) => void
|
||||
onUnarchive?: (id: number) => void
|
||||
onDelete?: (trip: DashboardTrip) => void
|
||||
onClick: (trip: DashboardTrip) => void
|
||||
t: (key: string, params?: Record<string, string | number | null>) => string
|
||||
locale: string
|
||||
@@ -460,16 +461,16 @@ function ArchivedRow({ trip, onEdit, onUnarchive, onDelete, onClick, t, locale,
|
||||
</div>
|
||||
{(!!trip.is_owner || isAdmin) && (
|
||||
<div style={{ display: 'flex', gap: 4, flexShrink: 0 }} onClick={e => e.stopPropagation()}>
|
||||
<button onClick={() => onUnarchive(trip.id)} title={t('dashboard.restore')} style={{ padding: '4px 8px', borderRadius: 8, border: '1px solid var(--border-primary)', background: 'var(--bg-card)', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, color: 'var(--text-muted)' }}
|
||||
{onUnarchive && <button onClick={() => onUnarchive(trip.id)} title={t('dashboard.restore')} style={{ padding: '4px 8px', borderRadius: 8, border: '1px solid var(--border-primary)', background: 'var(--bg-card)', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, color: 'var(--text-muted)' }}
|
||||
onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--text-faint)'; e.currentTarget.style.color = 'var(--text-primary)' }}
|
||||
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.color = 'var(--text-muted)' }}>
|
||||
<ArchiveRestore size={12} /> {t('dashboard.restore')}
|
||||
</button>
|
||||
<button onClick={() => onDelete(trip)} title={t('common.delete')} style={{ padding: '4px 8px', borderRadius: 8, border: '1px solid var(--border-primary)', background: 'var(--bg-card)', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, color: 'var(--text-faint)' }}
|
||||
</button>}
|
||||
{onDelete && <button onClick={() => onDelete(trip)} title={t('common.delete')} style={{ padding: '4px 8px', borderRadius: 8, border: '1px solid var(--border-primary)', background: 'var(--bg-card)', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, color: 'var(--text-faint)' }}
|
||||
onMouseEnter={e => { e.currentTarget.style.borderColor = '#fecaca'; e.currentTarget.style.color = '#ef4444' }}
|
||||
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.color = 'var(--text-faint)' }}>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</button>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -554,6 +555,7 @@ export default function DashboardPage(): React.ReactElement {
|
||||
const { demoMode, user } = useAuthStore()
|
||||
const isAdmin = user?.role === 'admin'
|
||||
const { settings, updateSetting } = useSettingsStore()
|
||||
const can = useCanDo()
|
||||
const dm = settings.dark_mode
|
||||
const dark = dm === true || dm === 'dark' || (dm === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
const showCurrency = settings.dashboard_currency !== 'off'
|
||||
@@ -681,7 +683,7 @@ export default function DashboardPage(): React.ReactElement {
|
||||
title={viewMode === 'grid' ? t('dashboard.listView') : t('dashboard.gridView')}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
padding: '0 14px',
|
||||
padding: '0 14px', height: 37,
|
||||
background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 12,
|
||||
cursor: 'pointer', color: 'var(--text-faint)', fontFamily: 'inherit',
|
||||
transition: 'background 0.15s, border-color 0.15s',
|
||||
@@ -696,7 +698,7 @@ export default function DashboardPage(): React.ReactElement {
|
||||
onClick={() => setShowWidgetSettings(s => s ? false : true)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
padding: '0 14px',
|
||||
padding: '0 14px', height: 37,
|
||||
background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 12,
|
||||
cursor: 'pointer', color: 'var(--text-faint)', fontFamily: 'inherit',
|
||||
transition: 'background 0.15s, border-color 0.15s',
|
||||
@@ -706,7 +708,7 @@ export default function DashboardPage(): React.ReactElement {
|
||||
>
|
||||
<Settings size={15} />
|
||||
</button>
|
||||
<button
|
||||
{can('trip_create') && <button
|
||||
onClick={() => { setEditingTrip(null); setShowForm(true) }}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 7, padding: '9px 18px',
|
||||
@@ -718,7 +720,7 @@ export default function DashboardPage(): React.ReactElement {
|
||||
onMouseLeave={e => e.currentTarget.style.opacity = '1'}
|
||||
>
|
||||
<Plus size={15} /> {t('dashboard.newTrip')}
|
||||
</button>
|
||||
</button>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -783,12 +785,12 @@ export default function DashboardPage(): React.ReactElement {
|
||||
<p style={{ margin: '0 0 24px', fontSize: 14, color: '#9ca3af', maxWidth: 340, marginLeft: 'auto', marginRight: 'auto' }}>
|
||||
{t('dashboard.emptyText')}
|
||||
</p>
|
||||
<button
|
||||
{can('trip_create') && <button
|
||||
onClick={() => { setEditingTrip(null); setShowForm(true) }}
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: 7, padding: '10px 22px', background: 'var(--accent)', color: 'var(--accent-text)', border: 'none', borderRadius: 12, fontSize: 14, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}
|
||||
>
|
||||
<Plus size={16} /> {t('dashboard.emptyButton')}
|
||||
</button>
|
||||
</button>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -797,9 +799,9 @@ export default function DashboardPage(): React.ReactElement {
|
||||
<SpotlightCard
|
||||
trip={spotlight}
|
||||
t={t} locale={locale} dark={dark} isAdmin={isAdmin}
|
||||
onEdit={tr => { setEditingTrip(tr); setShowForm(true) }}
|
||||
onDelete={handleDelete}
|
||||
onArchive={handleArchive}
|
||||
onEdit={(can('trip_edit', spotlight) || can('trip_cover_upload', spotlight)) ? tr => { setEditingTrip(tr); setShowForm(true) } : undefined}
|
||||
onDelete={can('trip_delete', spotlight) ? handleDelete : undefined}
|
||||
onArchive={can('trip_archive', spotlight) ? handleArchive : undefined}
|
||||
onClick={tr => navigate(`/trips/${tr.id}`)}
|
||||
/>
|
||||
)}
|
||||
@@ -813,9 +815,9 @@ export default function DashboardPage(): React.ReactElement {
|
||||
key={trip.id}
|
||||
trip={trip}
|
||||
t={t} locale={locale} isAdmin={isAdmin}
|
||||
onEdit={tr => { setEditingTrip(tr); setShowForm(true) }}
|
||||
onDelete={handleDelete}
|
||||
onArchive={handleArchive}
|
||||
onEdit={(can('trip_edit', trip) || can('trip_cover_upload', trip)) ? tr => { setEditingTrip(tr); setShowForm(true) } : undefined}
|
||||
onDelete={can('trip_delete', trip) ? handleDelete : undefined}
|
||||
onArchive={can('trip_archive', trip) ? handleArchive : undefined}
|
||||
onClick={tr => navigate(`/trips/${tr.id}`)}
|
||||
/>
|
||||
))}
|
||||
@@ -827,9 +829,9 @@ export default function DashboardPage(): React.ReactElement {
|
||||
key={trip.id}
|
||||
trip={trip}
|
||||
t={t} locale={locale} isAdmin={isAdmin}
|
||||
onEdit={tr => { setEditingTrip(tr); setShowForm(true) }}
|
||||
onDelete={handleDelete}
|
||||
onArchive={handleArchive}
|
||||
onEdit={(can('trip_edit', trip) || can('trip_cover_upload', trip)) ? tr => { setEditingTrip(tr); setShowForm(true) } : undefined}
|
||||
onDelete={can('trip_delete', trip) ? handleDelete : undefined}
|
||||
onArchive={can('trip_archive', trip) ? handleArchive : undefined}
|
||||
onClick={tr => navigate(`/trips/${tr.id}`)}
|
||||
/>
|
||||
))}
|
||||
@@ -857,9 +859,9 @@ export default function DashboardPage(): React.ReactElement {
|
||||
key={trip.id}
|
||||
trip={trip}
|
||||
t={t} locale={locale} isAdmin={isAdmin}
|
||||
onEdit={tr => { setEditingTrip(tr); setShowForm(true) }}
|
||||
onUnarchive={handleUnarchive}
|
||||
onDelete={handleDelete}
|
||||
onEdit={(can('trip_edit', trip) || can('trip_cover_upload', trip)) ? tr => { setEditingTrip(tr); setShowForm(true) } : undefined}
|
||||
onUnarchive={can('trip_archive', trip) ? handleUnarchive : undefined}
|
||||
onDelete={can('trip_delete', trip) ? handleDelete : undefined}
|
||||
onClick={tr => navigate(`/trips/${tr.id}`)}
|
||||
/>
|
||||
))}
|
||||
|
||||
52
client/src/store/permissionsStore.ts
Normal file
52
client/src/store/permissionsStore.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { create } from 'zustand'
|
||||
import { useAuthStore } from './authStore'
|
||||
|
||||
export type PermissionLevel = 'admin' | 'trip_owner' | 'trip_member' | 'everybody'
|
||||
|
||||
/** Minimal trip shape used by permission checks — accepts both Trip and DashboardTrip */
|
||||
type TripOwnerContext = { user_id?: unknown; owner_id?: unknown; is_owner?: unknown }
|
||||
|
||||
interface PermissionsState {
|
||||
permissions: Record<string, PermissionLevel>
|
||||
setPermissions: (perms: Record<string, PermissionLevel>) => void
|
||||
}
|
||||
|
||||
export const usePermissionsStore = create<PermissionsState>((set) => ({
|
||||
permissions: {},
|
||||
setPermissions: (perms) => set({ permissions: perms }),
|
||||
}))
|
||||
|
||||
/**
|
||||
* Hook that returns a permission checker bound to the current user.
|
||||
* Usage: const can = useCanDo(); can('trip_create') or can('file_upload', trip)
|
||||
*/
|
||||
export function useCanDo() {
|
||||
const perms = usePermissionsStore((s: PermissionsState) => s.permissions)
|
||||
const user = useAuthStore((s) => s.user)
|
||||
|
||||
return function can(
|
||||
actionKey: string,
|
||||
trip?: TripOwnerContext | null,
|
||||
): boolean {
|
||||
if (!user) return false
|
||||
if (user.role === 'admin') return true
|
||||
|
||||
const level = perms[actionKey]
|
||||
if (!level) return true // not configured = allow
|
||||
|
||||
// Support both Trip (owner_id) and DashboardTrip/server response (user_id)
|
||||
const tripOwnerId = (trip?.user_id as number | undefined) ?? (trip?.owner_id as number | undefined) ?? null
|
||||
const isOwnerFlag = trip?.is_owner === true || trip?.is_owner === 1
|
||||
const isOwner = isOwnerFlag || (tripOwnerId !== null && tripOwnerId === user.id)
|
||||
const isMember = !isOwner && trip != null
|
||||
|
||||
switch (level) {
|
||||
case 'admin': return false
|
||||
case 'trip_owner': return isOwner
|
||||
case 'trip_member': return isOwner || isMember
|
||||
case 'everybody': return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user