Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: find tickets by evaluation create time #943

Merged
merged 6 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion next/api/src/model/Ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
hasManyThroughPointer,
hasManyThroughPointerArray,
serialize,
AuthOptions,
} from '@/orm';
import { TicketUpdater, UpdateOptions } from '@/ticket/TicketUpdater';
import htmlify from '@/utils/htmlify';
Expand Down Expand Up @@ -62,6 +61,14 @@ export class Status {
export interface Evaluation {
star: number;
content: string;
selections?: string[];
/**
* 评价时间
*
* 不用 `createdAt` 是因为 API 对于名为 `createdAt` 的字段始终返回 string 类型的值
* 这会导致获取的值与最初设置的值类型不一致, 且 JS SDK 并未兼容这一行为
*/
ts?: Date;
}

export interface LatestReply extends Omit<TinyReplyInfo, 'objectId'> {
Expand Down
14 changes: 12 additions & 2 deletions next/api/src/router/ticket-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,19 @@ router.get('/realtime', parseRange('createdAt'), async (ctx) => {
.where('groupId', params['groupId'], 'in')
.where('status', params['status'], 'in')
.where('categoryId', categoryIds, 'in')
.where('ticketCreatedAt', params.createdAtFrom, '>')
.where('ticketCreatedAt', params.createdAtTo, '<')
.where('ticketCreatedAt', params.createdAtFrom, '>=')
.where('ticketCreatedAt', params.createdAtTo, '<=')
.where(new FunctionColumn(`JSONExtractInt(evaluation,'star')`), params['evaluation.star'])
.where(
new FunctionColumn(`JSONExtractString(evaluation,'ts','iso')`),
params['evaluation.ts']?.[0],
'>='
)
.where(
new FunctionColumn(`JSONExtractString(evaluation,'ts','iso')`),
params['evaluation.ts']?.[1],
'<='
)
.where(
new FunctionColumn(
`arrayExists( v ->${privateTagCondition
Expand Down
18 changes: 17 additions & 1 deletion next/api/src/router/ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const ticketFiltersSchema = yup.object({
participantId: yup.csv(yup.string().required()),
status: yup.csv(yup.number().oneOf(statuses).required()),
'evaluation.star': yup.number().oneOf([0, 1]),
'evaluation.ts': yup.dateRange(),
createdAtFrom: yup.date(),
createdAtTo: yup.date(),
tagKey: yup.string(),
Expand Down Expand Up @@ -191,6 +192,15 @@ router.get(
if (params['evaluation.star'] !== undefined) {
query.where('evaluation.star', '==', params['evaluation.star']);
}
if (params['evaluation.ts']) {
const [from, to] = params['evaluation.ts'];
if (from) {
query.where('evaluation.ts', '>=', from);
}
if (to) {
query.where('evaluation.ts', '<=', to);
}
}
if (params.createdAtFrom) {
query.where('createdAt', '>=', params.createdAtFrom);
}
Expand Down Expand Up @@ -354,6 +364,11 @@ router.get(
if (params['evaluation.star'] !== undefined) {
addEqCondition('evaluation.star', params['evaluation.star']);
}
if (params['evaluation.ts']) {
const from = params['evaluation.ts'][0]?.toISOString() ?? '*';
const to = params['evaluation.ts'][1]?.toISOString() ?? '*';
conditions.push(`evaluation.ts:[${from} TO ${to}]`);
}
if (params.createdAtFrom || params.createdAtTo) {
const from = params.createdAtFrom?.toISOString() ?? '*';
const to = params.createdAtTo?.toISOString() ?? '*';
Expand Down Expand Up @@ -845,6 +860,7 @@ router.patch('/:id', async (ctx) => {
nickname: currentUser.name,
})
).unescape,
ts: new Date(),
});
}

Expand Down Expand Up @@ -1208,4 +1224,4 @@ router.post('/search-custom-field', customerServiceOnly, async (ctx) => {
ctx.body = tickets.map((t) => new TicketListItemResponse(t));
});

export default router;
export default router;
10 changes: 10 additions & 0 deletions next/api/src/ticket/export/ExportTicket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface FilterOptions {
groupId?: string[];
status?: number[];
'evaluation.star'?: number;
'evaluation.ts'?: (Date | string | undefined | null)[];
createdAtFrom?: string | Date;
createdAtTo?: string | Date;
tagKey?: string;
Expand Down Expand Up @@ -109,6 +110,15 @@ const createBaseTicketQuery = async (params: FilterOptions, sortItems?: SortItem
if (params['evaluation.star'] !== undefined) {
query.where('evaluation.star', '==', params['evaluation.star']);
}
if (params['evaluation.ts']) {
const [from, to] = params['evaluation.ts'];
if (from) {
query.where('evaluation.ts', '>=', new Date(from));
}
if (to) {
query.where('evaluation.ts', '<=', new Date(to));
}
}
if (params.createdAtFrom) {
query.where('createdAt', '>=', new Date(params.createdAtFrom));
}
Expand Down
24 changes: 24 additions & 0 deletions next/api/src/utils/yup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,27 @@ export const csv: typeof yup.array = (type) => {
});
return schema as any;
};

export const dateRange = () => {
const schema = yup.array(yup.date()).transform((value) => {
if (value[0] || value[1]) {
// filter [undefined, undefined];
return value;
}
});
schema.transforms.unshift((value) => {
if (typeof value === 'string') {
return value
.split('..')
.slice(0, 2)
.map((v) => {
if (!v || v === '*') {
return undefined;
} else {
return v;
}
});
}
});
return schema;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useMemo, useState } from 'react';
import moment, { Moment } from 'moment';
import { DatePicker, Select } from 'antd';

const { RangePicker } = DatePicker;

const EMPTY_VALUE = '';
const RANGE_VALUE = 'range';

const options = [
{ value: EMPTY_VALUE, label: '所有时间' },
{ value: 'today', label: '今天' },
{ value: 'yesterday', label: '昨天' },
{ value: 'week', label: '本周' },
{ value: 'month', label: '本月' },
{ value: 'lastMonth', label: '上月' },
{ value: RANGE_VALUE, label: '选择时间段' },
];

interface PresetRangePickerProps {
value?: string;
onChange: (value?: string) => void;
disabled?: boolean;
}

export function PresetRangePicker({ value, onChange, disabled }: PresetRangePickerProps) {
const rangeValue = useMemo(() => {
if (value?.includes('..')) {
return value.split('..').map((str) => moment(str));
}
}, [value]);

const [rangeMode, setRangeMode] = useState(rangeValue !== undefined);

const handleChange = (value: string) => {
if (value === RANGE_VALUE) {
setRangeMode(true);
return;
}
setRangeMode(false);
onChange(value === EMPTY_VALUE ? undefined : value);
};

const handleChangeRange = (range: [Moment, Moment] | null) => {
if (!range) {
onChange(undefined);
return;
}
const [starts, ends] = range;
onChange(`${starts.toISOString()}..${ends.toISOString()}`);
};

const showRangePicker = rangeMode || rangeValue !== undefined;
return (
<>
<Select
className="w-full"
options={options}
value={showRangePicker ? RANGE_VALUE : value ?? EMPTY_VALUE}
onChange={handleChange}
disabled={disabled}
/>
{showRangePicker && (
<div className="pl-2 border-l border-gray-300 border-dashed">
<div className="my-2 text-[#475867] text-sm font-medium">时间段</div>
<RangePicker
className="w-full"
value={rangeValue as any}
onChange={handleChangeRange as any}
showTime={{
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
}}
/>
</div>
)}
</>
);
}
14 changes: 11 additions & 3 deletions next/web/src/App/Admin/Tickets/Filter/FilterForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { FieldFilters, Filters, NormalFilters } from '../useTicketFilter';
import { AssigneeSelect } from './AssigneeSelect';
import { GroupSelect } from './GroupSelect';
import { TagSelect } from './TagSelect';
import { CreatedAtSelect } from './CreatedAtSelect';
import { CategorySelect } from './CategorySelect';
import { StatusSelect } from './StatusSelect';
import { EvaluationStarSelect } from './EvaluationStarSelect';
Expand All @@ -17,6 +16,7 @@ import { TicketLanguages } from '@/i18n/locales';
import { TicketFieldSchema } from '@/api/ticket-field';
import { FieldSelect, OptionTypes, TextTypes } from './FieldSelect';
import { MetadataList } from './MetadataList';
import { PresetRangePicker } from './PresetRangePicker';

function Field({ title, children }: PropsWithChildren<{ title: React.ReactNode }>) {
return (
Expand Down Expand Up @@ -52,6 +52,7 @@ const NormalFieldForm = ({ filters, merge, onSubmit }: FilterFormItemProps<Norma
privateTagValue,
language,
star,
'evaluation.ts': evaluation_ts,
status,
tagKey,
tagValue,
Expand All @@ -62,7 +63,7 @@ const NormalFieldForm = ({ filters, merge, onSubmit }: FilterFormItemProps<Norma
return (
<>
<Field title="创建时间">
<CreatedAtSelect value={createdAt} onChange={(createdAt) => merge({ createdAt })} />
<PresetRangePicker value={createdAt} onChange={(createdAt) => merge({ createdAt })} />
</Field>
<Field title="关键词">
<Input
Expand Down Expand Up @@ -120,6 +121,13 @@ const NormalFieldForm = ({ filters, merge, onSubmit }: FilterFormItemProps<Norma
<EvaluationStarSelect value={star} onChange={(star) => merge({ star })} />
</Field>

<Field title="评价时间">
<PresetRangePicker
value={evaluation_ts}
onChange={(value) => merge({ 'evaluation.ts': value })}
/>
</Field>

<Field title="标签">
<TagSelect value={{ tagKey, tagValue, privateTagKey, privateTagValue }} onChange={merge} />
</Field>
Expand Down Expand Up @@ -161,7 +169,7 @@ const CustomFieldForm = ({ filters, merge, onSubmit }: FilterFormItemProps<Field
return (
<>
<Field title="创建时间">
<CreatedAtSelect value={createdAt} onChange={(createdAt) => merge({ createdAt })} />
<PresetRangePicker value={createdAt} onChange={(createdAt) => merge({ createdAt })} />
</Field>
<Field title="工单选项">
<FieldSelect value={fieldId} onChangeWithData={setField} />
Expand Down
2 changes: 2 additions & 0 deletions next/web/src/App/Admin/Tickets/Filter/useTicketFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface NormalFilters extends CommonFilters {
rootCategoryId?: string;
status?: number[];
star?: number;
'evaluation.ts'?: string;
language?: string[];
where?: Record<string, any>;
}
Expand Down Expand Up @@ -78,6 +79,7 @@ const deserializeFilters = (params: Record<string, string | undefined>): Filters
'privateTagKey',
'privateTagValue',
'rootCategoryId',
'evaluation.ts',

// field
'fieldId',
Expand Down
11 changes: 11 additions & 0 deletions next/web/src/api/ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface FetchTicketFilters {
language?: string[];
rootCategoryId?: string;
star?: number;
'evaluation.ts'?: string;
createdAt?: string;
status?: number | number[];
tagKey?: string;
Expand Down Expand Up @@ -96,6 +97,16 @@ export function encodeTicketFilters(filters: FetchTicketFilters) {
if (!isEmpty(filters.where)) {
params.where = JSON.stringify(filters.where);
}
if (filters['evaluation.ts']) {
const dateRange = decodeDateRange(filters['evaluation.ts']);
if (dateRange && (dateRange.from || dateRange.to)) {
// "2021-08-01..2021-08-31", "2021-08-01..*", etc.
params['evaluation.ts'] = [
dateRange.from?.toISOString() ?? '*',
dateRange.to?.toISOString() ?? '*',
].join('..');
}
}
return params;
}

Expand Down