Skip to content

Commit

Permalink
feat: find tickets by evaluation create time (#943)
Browse files Browse the repository at this point in the history
  • Loading branch information
sdjdd authored Oct 24, 2023
1 parent 01dced6 commit 9a8f99c
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 7 deletions.
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 @@ -48,6 +48,7 @@ export interface FetchTicketFilters {
language?: string[];
rootCategoryId?: string;
star?: number;
'evaluation.ts'?: string;
createdAt?: string;
status?: number | number[];
tagKey?: string;
Expand Down Expand Up @@ -102,6 +103,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

0 comments on commit 9a8f99c

Please sign in to comment.