mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-11 13:28:57 +00:00
feat: translate reports, sla badge, view builder and search
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<p class="text-2xl flex items-center">{{ title }}</p>
|
||||
<div class="bg-green-100/70 flex items-center space-x-2 px-1 rounded">
|
||||
<span class="blinking-dot"></span>
|
||||
<p class="uppercase text-xs">Live</p>
|
||||
<p class="uppercase text-xs">{{ $t('report.live') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between pr-32">
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
<template>
|
||||
<LineChart :data="data" index="date" :categories="['New conversations', 'Resolved conversations']"
|
||||
:x-formatter="xFormatter" :y-formatter="yFormatter" />
|
||||
<LineChart
|
||||
:data="data"
|
||||
index="date"
|
||||
:categories="[t('report.chart.newConversations'), t('report.chart.resolvedConversations')]"
|
||||
:x-formatter="xFormatter"
|
||||
:y-formatter="yFormatter"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { LineChart } from '@/components/ui/chart-line'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
const { t } = useI18n()
|
||||
|
||||
const xFormatter = (tick) => {
|
||||
return props.data[tick]?.date ?? ''
|
||||
@@ -19,5 +26,4 @@ const xFormatter = (tick) => {
|
||||
const yFormatter = (tick) => {
|
||||
return Number.isInteger(tick) ? tick : ''
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<Separator orientation="vertical" />
|
||||
<Input
|
||||
v-model="model"
|
||||
placeholder="Search"
|
||||
:placeholder="t('search.search')"
|
||||
class="w-full border-none shadow-none focus:ring-0 focus:ring-offset-0"
|
||||
/>
|
||||
</div>
|
||||
@@ -19,7 +19,9 @@ import { Separator } from '@/components/ui/separator'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||
import { Search } from 'lucide-vue-next'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const model = defineModel(() => '')
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
<template>
|
||||
<div class="max-w-5xl mx-auto p-6 bg-background min-h-screen">
|
||||
<div v-if="isEmptyResults" class="text-center py-16 rounded-lg">
|
||||
<SearchXIcon class="h-20 w-20 text-muted-foreground mx-auto mb-6" />
|
||||
<h2 class="text-2xl font-bold text-foreground mb-3">No results found</h2>
|
||||
<p class="text-muted-foreground text-lg max-w-md mx-auto">
|
||||
We couldn't find any matches. Try adjusting your search query.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="space-y-8">
|
||||
<div class="space-y-8">
|
||||
<div
|
||||
v-for="(items, type) in results"
|
||||
:key="type"
|
||||
@@ -18,7 +11,9 @@
|
||||
</h2>
|
||||
|
||||
<div v-if="items.length === 0" class="p-6 text-muted-foreground">
|
||||
No {{ type }} found
|
||||
{{ $t('search.noResults', {
|
||||
name: type
|
||||
}) }}
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-border">
|
||||
@@ -82,21 +77,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { SearchXIcon, ChevronRightIcon, ClockIcon } from 'lucide-vue-next'
|
||||
import { ChevronRightIcon, ClockIcon } from 'lucide-vue-next'
|
||||
import { format, parseISO } from 'date-fns'
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
results: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const isEmptyResults = computed(() => {
|
||||
return Object.values(props.results).every((arr) => arr.length === 0)
|
||||
})
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
const date = parseISO(dateString)
|
||||
return format(date, 'MMM d, yyyy HH:mm')
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<!-- Overdue-->
|
||||
<span v-if="sla?.status === 'overdue'" key="overdue" class="sla-badge box sla-overdue">
|
||||
<AlertCircle size="12" class="text-red-800" />
|
||||
<span class="sla-text text-red-800"
|
||||
>{{ label }} Overdue
|
||||
<span v-if="showExtra">by {{ sla.value }}</span>
|
||||
<span class="sla-text text-red-800">
|
||||
<span v-if="!showExtra">{{ label }} {{ $t('sla.overdue') }}</span>
|
||||
<span v-else>{{ label }} {{ $t('sla.overdueBy') }} {{ sla.value }} </span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
class="sla-badge box sla-hit"
|
||||
>
|
||||
<CheckCircle size="12" />
|
||||
<span class="sla-text">{{ label }} SLA met</span>
|
||||
<span class="sla-text">{{ label }} {{ $t('sla.met') }}</span>
|
||||
</span>
|
||||
|
||||
<!-- Remaining -->
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<!-- Field -->
|
||||
<Select v-model="modelFilter.field">
|
||||
<SelectTrigger class="bg-transparent hover:bg-slate-100 w-full">
|
||||
<SelectValue placeholder="Field" />
|
||||
<SelectValue :placeholder="t('form.field.selectField')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -23,7 +23,7 @@
|
||||
<!-- Operator -->
|
||||
<Select v-model="modelFilter.operator" v-if="modelFilter.field">
|
||||
<SelectTrigger class="bg-transparent hover:bg-slate-100 w-full">
|
||||
<SelectValue placeholder="Operator" />
|
||||
<SelectValue :placeholder="t('form.field.selectOperator')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -41,7 +41,7 @@
|
||||
v-if="getFieldOptions(modelFilter).length > 0"
|
||||
v-model="modelFilter.value"
|
||||
:items="getFieldOptions(modelFilter)"
|
||||
placeholder="Select"
|
||||
:placeholder="t('form.field.select')"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<div v-if="modelFilter.field === 'assigned_user_id'">
|
||||
@@ -65,7 +65,7 @@
|
||||
</template>
|
||||
|
||||
<template #selected="{ selected }">
|
||||
<div v-if="!selected">Select value</div>
|
||||
<div v-if="!selected">{{ $t('form.field.selectValue') }}</div>
|
||||
<div v-if="modelFilter.field === 'assigned_user_id'">
|
||||
<div class="flex items-center gap-2">
|
||||
<div v-if="selected" class="flex items-center gap-1">
|
||||
@@ -96,7 +96,7 @@
|
||||
v-else
|
||||
v-model="modelFilter.value"
|
||||
class="bg-transparent hover:bg-slate-100"
|
||||
placeholder="Value"
|
||||
:placeholder="t('form.field.value')"
|
||||
type="text"
|
||||
/>
|
||||
</template>
|
||||
@@ -114,11 +114,16 @@
|
||||
|
||||
<div class="flex items-center justify-between pt-3">
|
||||
<Button variant="ghost" size="sm" @click="addFilter" class="text-slate-600">
|
||||
<Plus class="w-3 h-3 mr-1" /> Add filter
|
||||
<Plus class="w-3 h-3 mr-1" />
|
||||
{{
|
||||
$t('globals.messages.add', {
|
||||
name: $t('globals.entities.filter')
|
||||
})
|
||||
}}
|
||||
</Button>
|
||||
<div class="flex gap-2" v-if="showButtons">
|
||||
<Button variant="ghost" @click="clearFilters">Reset</Button>
|
||||
<Button @click="applyFilters">Apply</Button>
|
||||
<Button variant="ghost" @click="clearFilters">{{ $t('globals.buttons.reset') }}</Button>
|
||||
<Button @click="applyFilters">{{ $t('globals.buttons.apply') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,6 +143,7 @@ import { Plus, X } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ComboBox from '@/components/ui/combobox/ComboBox.vue'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -150,7 +156,7 @@ const props = defineProps({
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const emit = defineEmits(['apply', 'clear'])
|
||||
const modelValue = defineModel('modelValue', { required: false, default: () => [] })
|
||||
|
||||
|
||||
@@ -2,26 +2,29 @@
|
||||
<Dialog :open="openDialog" @update:open="openDialog = false">
|
||||
<DialogContent class="min-w-[40%] min-h-[30%]">
|
||||
<DialogHeader class="space-y-1">
|
||||
<DialogTitle>{{ view?.id ? 'Edit' : 'Create' }} view</DialogTitle>
|
||||
<DialogTitle
|
||||
>{{ view?.id ? $t('globals.buttons.edit') : $t('globals.buttons.create') }}
|
||||
view
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create and save custom filter views for quick access to your conversations.
|
||||
{{ $t('view.form.description') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<div class="grid gap-4 py-4">
|
||||
<FormField v-slot="{ componentField }" name="name">
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>{{ $t('form.field.name') }}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="name"
|
||||
class="col-span-3"
|
||||
placeholder="Name"
|
||||
placeholder=""
|
||||
v-bind="componentField"
|
||||
@keydown.enter.prevent="onSubmit"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Enter an unique name for your view.</FormDescription>
|
||||
<FormDescription>{{ $t('view.form.name.description') }}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
@@ -35,14 +38,14 @@
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription> Set one or more filters to customize view.</FormDescription>
|
||||
<FormDescription> {{ $t('view.form.filters.description') }}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" :disabled="isSubmitting" :isLoading="isSubmitting">
|
||||
{{ isSubmitting ? 'Saving...' : 'Save changes' }}
|
||||
{{ isSubmitting ? t('globals.buttons.saving') : t('globals.buttons.save') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
@@ -78,14 +81,15 @@ import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
import { handleHTTPError } from '@/utils/http'
|
||||
import { OPERATOR } from '@/constants/filterConfig.js'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { z } from 'zod'
|
||||
import api from '@/api'
|
||||
|
||||
const emitter = useEmitter()
|
||||
const { t } = useI18n()
|
||||
const openDialog = defineModel('openDialog', { required: false, default: false })
|
||||
const view = defineModel('view', { required: false, default: {} })
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
const { conversationsListFilters } = useConversationFilters()
|
||||
|
||||
const filterFields = computed(() =>
|
||||
@@ -103,22 +107,19 @@ const formSchema = toTypedSchema(
|
||||
id: z.number().optional(),
|
||||
name: z
|
||||
.string()
|
||||
.min(2, { message: 'Name must be at least 2 characters.' })
|
||||
.max(30, { message: 'Name cannot exceed 30 characters.' }),
|
||||
.min(2, { message: t('view.form.name.length') })
|
||||
.max(30, { message: t('view.form.name.length') }),
|
||||
filters: z
|
||||
.array(
|
||||
z.object({
|
||||
model: z.string({ required_error: 'Filter required' }),
|
||||
field: z.string({ required_error: 'Filter required' }),
|
||||
operator: z.string({ required_error: 'Filter required' }),
|
||||
model: z.string({ required_error: t('view.form.filter.required') }),
|
||||
field: z.string({ required_error: t('view.form.filter.required') }),
|
||||
operator: z.string({ required_error: t('view.form.filter.required') }),
|
||||
value: z.union([z.string(), z.number(), z.boolean()]).optional()
|
||||
})
|
||||
)
|
||||
.default([])
|
||||
.refine(
|
||||
(filters) => filters.length > 0,
|
||||
{ message: 'Please add at least one filter.' }
|
||||
)
|
||||
.refine((filters) => filters.length > 0, { message: t('view.form.filter.selectAtLeastOne') })
|
||||
.refine(
|
||||
(filters) =>
|
||||
filters.every(
|
||||
@@ -129,7 +130,7 @@ const formSchema = toTypedSchema(
|
||||
([OPERATOR.SET, OPERATOR.NOT_SET].includes(f.operator) || f.value)
|
||||
),
|
||||
{
|
||||
message: "Please make sure you've filled the filter fields correctly."
|
||||
message: t('view.form.filter.partiallyFilled')
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -161,7 +162,6 @@ const onSubmit = async () => {
|
||||
form.resetForm()
|
||||
} catch (error) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class="flex flex-col items-center justify-center flex-grow"
|
||||
v-if="$route.name === 'admin'"
|
||||
>
|
||||
<div>Select a section from the sidebar</div>
|
||||
<div>{{ $t('admin.empty') }}</div>
|
||||
</div>
|
||||
<router-view class="flex-grow" />
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,6 @@ export const useMacroStore = defineStore('macroStore', () => {
|
||||
macroList.value = response?.data?.data || []
|
||||
} catch (error) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
|
||||
@@ -21,7 +21,6 @@ export const useTagStore = defineStore('tags', () => {
|
||||
tags.value = response?.data?.data || []
|
||||
} catch (error) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
|
||||
@@ -20,7 +20,6 @@ export const useTeamStore = defineStore('team', () => {
|
||||
teams.value = response?.data?.data || []
|
||||
} catch (error) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
|
||||
@@ -65,7 +65,6 @@ export const useUserStore = defineStore('user', () => {
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 401) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Could not fetch current user',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
|
||||
@@ -20,7 +20,6 @@ export const useUsersStore = defineStore('users', () => {
|
||||
users.value = response?.data?.data || []
|
||||
} catch (error) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<Spinner v-if="isLoading" />
|
||||
<div class="space-y-4">
|
||||
<div class="text-sm text-gray-500 text-right">
|
||||
Last updated: {{ new Date(lastUpdate).toLocaleTimeString() }}
|
||||
{{ $t('report.lastUpdated') }}: {{ new Date(lastUpdate).toLocaleTimeString() }}
|
||||
</div>
|
||||
<div class="mt-7 flex w-full space-x-4">
|
||||
<Card title="Open conversations" :counts="cardCounts" :labels="agentCountCardsLabels" />
|
||||
@@ -19,10 +19,10 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="rounded-lg box w-full p-5 bg-white">
|
||||
<LineChart :data="chartData.processedData"></LineChart>
|
||||
<LineChart :data="chartData.processedData" />
|
||||
</div>
|
||||
<div class="rounded-lg box w-full p-5 bg-white">
|
||||
<BarChart :data="chartData.status_summary"></BarChart>
|
||||
<BarChart :data="chartData.status_summary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,9 +38,11 @@ import Card from '@/features/reports/DashboardCard.vue'
|
||||
import LineChart from '@/features/reports/DashboardLineChart.vue'
|
||||
import BarChart from '@/features/reports/DashboardBarChart.vue'
|
||||
import Spinner from '@/components/ui/spinner/Spinner.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import api from '@/api'
|
||||
|
||||
const emitter = useEmitter()
|
||||
const { t } = useI18n()
|
||||
const isLoading = ref(false)
|
||||
const cardCounts = ref({})
|
||||
const chartData = ref({})
|
||||
@@ -48,16 +50,16 @@ const lastUpdate = ref(new Date())
|
||||
let updateInterval
|
||||
|
||||
const agentCountCardsLabels = {
|
||||
open: 'Total',
|
||||
awaiting_response: 'Awaiting Response',
|
||||
unassigned: 'Unassigned',
|
||||
pending: 'Pending'
|
||||
open: t('report.open'),
|
||||
awaiting_response: t('report.awaiting_response'),
|
||||
unassigned: t('report.unassigned'),
|
||||
pending: t('report.pending')
|
||||
}
|
||||
|
||||
const agentStatusLabels = {
|
||||
agents_online: 'Online',
|
||||
agents_offline: 'Offline',
|
||||
agents_away: 'Away'
|
||||
agents_online: t('user.online'),
|
||||
agents_offline: t('user.offline'),
|
||||
agents_away: t('user.away')
|
||||
}
|
||||
|
||||
const agentStatusCounts = ref({
|
||||
@@ -106,7 +108,6 @@ const getCardStats = async () => {
|
||||
})
|
||||
.catch((error) => {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
@@ -134,17 +135,16 @@ const getDashboardCharts = async () => {
|
||||
// Process data for all dates
|
||||
chartData.value.processedData = uniqueDates.map((date) => ({
|
||||
date,
|
||||
'New conversations':
|
||||
[t('report.chart.newConversations')]:
|
||||
chartData.value.new_conversations.find((item) => item.date === date)?.count || 0,
|
||||
'Resolved conversations':
|
||||
chartData.value.resolved_conversations.find((item) => item.date === date)?.count || 0,
|
||||
[t('report.chart.resolvedConversations')]:
|
||||
chartData.value.resolved_conversations.find((item) => item.date === date)?.count || 0
|
||||
}))
|
||||
|
||||
chartData.value.status_summary = resp.data.data.status_summary || []
|
||||
})
|
||||
.catch((error) => {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<div v-else-if="error" class="mt-8 text-center space-y-4">
|
||||
<p class="text-lg text-destructive">{{ error }}</p>
|
||||
<Button @click="handleSearch"> Try again </Button>
|
||||
<Button @click="handleSearch"> {{ $t('search.tryAgain') }} </Button>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
@@ -15,7 +15,11 @@
|
||||
v-if="searchPerformed && totalResults === 0"
|
||||
class="mt-8 text-center text-muted-foreground"
|
||||
>
|
||||
No results found for "{{ searchQuery }}". Try a different search term.
|
||||
{{
|
||||
$t('search.noResultsForQuery', {
|
||||
query: searchQuery
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<SearchResults v-else-if="searchPerformed" :results="results" class="h-full" />
|
||||
|
||||
@@ -23,12 +27,20 @@
|
||||
v-else-if="searchQuery.length > 0 && searchQuery.length < MIN_SEARCH_LENGTH"
|
||||
class="mt-8 text-center text-muted-foreground"
|
||||
>
|
||||
Please enter at least {{ MIN_SEARCH_LENGTH }} characters to search.
|
||||
{{
|
||||
$t('search.minQueryLength', {
|
||||
length: MIN_SEARCH_LENGTH
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<div v-else class="mt-16 text-center">
|
||||
<h2 class="text-2xl font-semibold text-primary mb-4">Search conversations</h2>
|
||||
<h2 class="text-2xl font-semibold text-primary mb-4">
|
||||
{{ $t('search.searchConversations') }}
|
||||
</h2>
|
||||
<p class="text-lg text-muted-foreground">
|
||||
Search by reference number, contact email address or messages in conversations.
|
||||
{{
|
||||
$t('search.searchBy')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+36
-2
@@ -51,6 +51,7 @@
|
||||
"globals.entities.sso": "SSO | SSOs",
|
||||
"globals.entities.hour": "Hour | Hours",
|
||||
"globals.entities.day": "Day | Days",
|
||||
"globals.entities.filter": "Filter | Filters",
|
||||
"globals.messages.adjustFilters": "Try adjusting your filters",
|
||||
"globals.messages.errorUploadingFile": "Error uploading file",
|
||||
"globals.messages.errorUpdating": "Error updating {name}",
|
||||
@@ -126,6 +127,9 @@
|
||||
"auth.csrfTokenMismatch": "CSRF token mismatch",
|
||||
"authz.permissionDenied": "Permission denied",
|
||||
"user.userAlreadyLoggedIn": "User already logged in",
|
||||
"user.online": "Online",
|
||||
"user.offline": "Offline",
|
||||
"user.away": "Away",
|
||||
"user.invalidEmailPassword": "Invalid email or password.",
|
||||
"user.accountDisabled": "Your account is disabled, please contact administrator",
|
||||
"user.cannotDeleteSystemUser": "Cannot delete system user",
|
||||
@@ -154,7 +158,6 @@
|
||||
"macro.partiallyApplied": "Macro partially applied",
|
||||
"macro.applied": "Macro applied",
|
||||
"sla.firstResponseTimeAfterResolution": "First response time cannot be after resolution time",
|
||||
"search.minQueryLength": "Minimum query should be at least {length} characters",
|
||||
"conversation.resolveWithoutAssignee": "Cannot resolve the conversation without an assigned user, Please assign a user before attempting to resolve",
|
||||
"conversation.notMemberOfTeam": "You're not a member of this team, Please refresh the page and try again",
|
||||
"conversation.viewPermissionDenied": "You do not have access to view this view",
|
||||
@@ -282,6 +285,7 @@
|
||||
"form.fields.setValue": "Set value",
|
||||
"form.fields.selectEvents": "Select events",
|
||||
"form.field.selectOperator": "Select operator",
|
||||
"form.field.value": "Value",
|
||||
"form.error.min": "Must be at least {min} characters",
|
||||
"form.error.max": "Must be at most {max} characters",
|
||||
"form.error.minmax": "Must be between {min} and {max} characters",
|
||||
@@ -290,6 +294,7 @@
|
||||
"form.error.description.required": "Description is required",
|
||||
"form.error.time.invalid": "Invalid time format (HH:mm)",
|
||||
"form.error.validUrl": "Invalid URL",
|
||||
"admin.empty": "Select a section from the sidebar",
|
||||
"admin.general.updated": "Settings updated successfully",
|
||||
"admin.general.site_name": "Site Name",
|
||||
"admin.general.site_name.description": "Name for your support desk.",
|
||||
@@ -530,11 +535,40 @@
|
||||
"globals.buttons.create": "Create",
|
||||
"globals.buttons.enable": "Enable",
|
||||
"globals.buttons.disable": "Disable",
|
||||
"globals.buttons.saving": "Saving...",
|
||||
"globals.buttons.back": "Back",
|
||||
"globals.buttons.edit": "Edit",
|
||||
"globals.buttons.new": "New",
|
||||
"globals.buttons.apply": "Apply",
|
||||
"globals.buttons.reset": "Reset",
|
||||
"globals.messages.atleastOneRecipient": "At least one recipient is required",
|
||||
"globals.messages.startTypingToSearch": "Start typing to search...",
|
||||
"globals.messages.goHourMinuteDuration": "Invalid duration format. Should be a number followed by h (hours), m (minutes).",
|
||||
"globals.messages.goDuration": "Invalid duration. Please use a valid duration format (e.g. 30s, 30m, 1h30m, 48h, etc.)"
|
||||
"globals.messages.goDuration": "Invalid duration. Please use a valid duration format (e.g. 30s, 30m, 1h30m, 48h, etc.)",
|
||||
"report.live": "Live",
|
||||
"report.lastUpdated": "Last updated",
|
||||
"report.total": "Total",
|
||||
"report.open": "Open",
|
||||
"report.awaiting_response": "Awaiting Response",
|
||||
"report.unassigned": "Unassigned",
|
||||
"report.pending": "Pending",
|
||||
"report.chart.newConversations": "New conversations",
|
||||
"report.chart.resolvedConversations": "Resolved conversations",
|
||||
"search.search": "Search",
|
||||
"search.noResults": "No {name} found.",
|
||||
"search.tryAgain": "Try again",
|
||||
"search.noResultsForQuery": "No results found for query `{query}`. Try a different search term.",
|
||||
"search.minQueryLength": " Please enter at least {length} characters to search.",
|
||||
"search.searchConversations": "Search conversations",
|
||||
"search.searchBy": "Search by reference number, contact email address or messages in conversations.",
|
||||
"sla.overdue": "Overdue",
|
||||
"sla.overdueBy": "Overdue by",
|
||||
"sla.met": "SLA met",
|
||||
"view.form.description": "Create and save custom filter views for quick access to your conversations.",
|
||||
"view.form.name.description": "Enter an unique name for your view.",
|
||||
"view.form.filters.description": "Set one or more filters to customize view.",
|
||||
"view.form.name.length": "View name should be between 2 and 30 characters.",
|
||||
"view.form.filter.required": "Filter required.",
|
||||
"view.form.filter.selectAtLeastOne": "Select at least one filter.",
|
||||
"view.form.filter.partiallyFilled": "Please make sure you've filled the filter fields correctly."
|
||||
}
|
||||
Reference in New Issue
Block a user