first commit
This commit is contained in:
commit
12e4f17b62
4633 files changed
+817125
No files matched your search
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { DashboardCard, useOAIQuery } from '@/shared';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
from: Date;
|
||||
to: Date;
|
||||
}
|
||||
|
||||
const CreateFeedbackPerIssueCard: React.FC<IProps> = (props) => {
|
||||
const { from, to, projectId } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data } = useOAIQuery({
|
||||
path: '/api/admin/statistics/feedback/issued-ratio',
|
||||
variables: { from: from.toISOString(), to: to.toISOString(), projectId },
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
return (
|
||||
<DashboardCard
|
||||
data={`${parseFloat(Math.abs((data?.ratio ?? 0) * 100).toFixed(1))}%`}
|
||||
title={t('dashboard-card.issue-ratio.title')}
|
||||
description={`${t('dashboard-card.issue-ratio.description', {
|
||||
targetDate: `${dayjs(from).format('YYYY/MM/DD')} - ${dayjs(to).format(
|
||||
'YYYY/MM/DD',
|
||||
)}`,
|
||||
})}`}
|
||||
type="issue"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateFeedbackPerIssueCard;
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxContent,
|
||||
ComboboxList,
|
||||
ComboboxSelectItem,
|
||||
ComboboxTrigger,
|
||||
Icon,
|
||||
} from '@ufb/react';
|
||||
|
||||
import {
|
||||
ChartCard,
|
||||
Legend,
|
||||
LineChart,
|
||||
useAllChannels,
|
||||
useOAIQuery,
|
||||
} from '@/shared';
|
||||
import type { Channel } from '@/entities/channel';
|
||||
|
||||
import { useLineChartData } from '../lib';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
from: Date;
|
||||
to: Date;
|
||||
}
|
||||
const FeedbackLineChartWrapper: React.FC<IProps> = (props) => {
|
||||
const { from, projectId, to } = props;
|
||||
const { data: channels } = useAllChannels(projectId);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [currentChannels, setCurrentChannels] = useState<Channel[]>([]);
|
||||
const dayCount = useMemo(() => dayjs(to).diff(from, 'day') + 1, [from, to]);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentChannels(channels?.items.slice(0, 5) ?? []);
|
||||
}, [channels]);
|
||||
|
||||
const { data } = useOAIQuery({
|
||||
path: '/api/admin/statistics/feedback',
|
||||
variables: {
|
||||
startDate: dayjs(from).startOf('day').format('YYYY-MM-DD'),
|
||||
endDate: dayjs(to).endOf('day').format('YYYY-MM-DD'),
|
||||
channelIds: currentChannels.map(({ id }) => id).join(','),
|
||||
interval: dayCount > 50 ? 'week' : 'day',
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { chartData, dataKeys } = useLineChartData(
|
||||
from,
|
||||
to,
|
||||
currentChannels,
|
||||
data?.channels ?? [],
|
||||
);
|
||||
|
||||
return (
|
||||
<ChartCard
|
||||
title={t('chart.feedback-trend.title')}
|
||||
description={`${t('chart.feedback-trend.description')} (${dayjs(
|
||||
from,
|
||||
).format('YYYY/MM/DD')} - ${dayjs(to).format('YYYY/MM/DD')})`}
|
||||
extra={
|
||||
<div className="flex gap-3">
|
||||
<Legend dataKeys={dataKeys} />
|
||||
<ChannelSelectCombobox
|
||||
currentChannels={currentChannels}
|
||||
setCurrentChannels={setCurrentChannels}
|
||||
channels={channels}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LineChart height={400} dataKeys={dataKeys} data={chartData} />
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
interface ChannelSelectComboboxProps {
|
||||
channels?: { items: Channel[] };
|
||||
currentChannels: Channel[];
|
||||
setCurrentChannels: React.Dispatch<React.SetStateAction<Channel[]>>;
|
||||
}
|
||||
|
||||
const ChannelSelectCombobox = (props: ChannelSelectComboboxProps) => {
|
||||
const { channels, currentChannels, setCurrentChannels } = props;
|
||||
return (
|
||||
<Combobox>
|
||||
<ComboboxTrigger>
|
||||
<Icon name="RiFilter3Line" />
|
||||
Filter
|
||||
</ComboboxTrigger>
|
||||
<ComboboxContent>
|
||||
<ComboboxList maxHeight="200px">
|
||||
{channels?.items.map((channel) => (
|
||||
<ComboboxSelectItem
|
||||
key={channel.id}
|
||||
value={String(channel.id)}
|
||||
checked={currentChannels.some(({ id }) => id === channel.id)}
|
||||
onSelect={() => {
|
||||
const isChecked = currentChannels.some(
|
||||
({ id }) => id === channel.id,
|
||||
);
|
||||
setCurrentChannels((prev) =>
|
||||
isChecked ? prev.filter(({ id }) => id !== channel.id)
|
||||
: prev.length === 5 ? [...prev.slice(1), channel]
|
||||
: [...prev, channel],
|
||||
);
|
||||
}}
|
||||
>
|
||||
{channel.name}
|
||||
</ComboboxSelectItem>
|
||||
))}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackLineChartWrapper;
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { default as IssueRank } from './issue-rank.ui';
|
||||
|
||||
export { default as TotalFeedbackCard } from './total-feedback-card.ui';
|
||||
export { default as TotalIssueCard } from './total-issue-card.ui';
|
||||
|
||||
export { default as CreateFeedbackPerIssueCard } from './create-feedback-per-issue-card.ui';
|
||||
|
||||
export { default as TodayFeedbackCard } from './today-feedback-card.ui';
|
||||
export { default as YesterdayFeedbackCard } from './yesterday-feedback-card.ui';
|
||||
export { default as SevenDaysFeedbackCard } from './seven-days-feedback-card.ui';
|
||||
export { default as ThirtyDaysFeedbackCard } from './thirty-days-feedback-card.ui';
|
||||
|
||||
export { default as TodayIssueCard } from './today-issue-card.ui';
|
||||
export { default as YesterdayIssueCard } from './yesterday-issue-card.ui';
|
||||
export { default as SevenDaysIssueCard } from './seven-days-issue-card.ui';
|
||||
export { default as ThirtyDaysIssueCard } from './thirty-days-issue-card.ui';
|
||||
|
||||
export { default as IssueLineChart } from './issue-line-chart.ui';
|
||||
|
||||
export { default as IssueBarChart } from './issue-bar-chart.ui';
|
||||
|
||||
export { default as IssueFeedbackLineChart } from './issue-feedback-line-chart.ui';
|
||||
export { default as FeedbackLineChart } from './feedback-line-chart.ui';
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { ToggleGroup, ToggleGroupItem } from '@ufb/react';
|
||||
|
||||
import {
|
||||
BarChart,
|
||||
ChartCard,
|
||||
ISSUES,
|
||||
Path,
|
||||
PieChart,
|
||||
useOAIQuery,
|
||||
} from '@/shared';
|
||||
import type { IssueStatus } from '@/entities/issue';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
from: Date;
|
||||
to: Date;
|
||||
}
|
||||
const COLOR_MAP: Record<IssueStatus, string> = {
|
||||
INIT: '#EAB308',
|
||||
IN_PROGRESS: '#0EA5E9',
|
||||
ON_REVIEW: '#22C55E',
|
||||
PENDING: '#6366F1',
|
||||
RESOLVED: '#71717A',
|
||||
};
|
||||
|
||||
const IssueBarChart: React.FC<IProps> = ({ projectId }) => {
|
||||
const { t } = useTranslation();
|
||||
const [type, setType] = useState('bar');
|
||||
|
||||
const { data: statisticsData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/issue/count-by-status',
|
||||
variables: { projectId },
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
const onChangeType = (type: 'bar' | 'pie') => (v: string) => {
|
||||
setType(v === '' ? type : v);
|
||||
};
|
||||
const data = ISSUES(t).map(({ key, name }) => ({
|
||||
name,
|
||||
value: +(
|
||||
statisticsData?.statistics.find((v) => v.status === key)?.count ?? 0
|
||||
),
|
||||
color: COLOR_MAP[key],
|
||||
}));
|
||||
const onClickIssue = (name: string | undefined) => {
|
||||
if (!name) return;
|
||||
const issue = ISSUES(t).find((v) => v.name === name);
|
||||
window.open(
|
||||
Path.ISSUE.replace('[projectId]', projectId.toString()) +
|
||||
'?queries=' +
|
||||
JSON.stringify([{ key: 'status', value: issue?.key, condition: 'IS' }]),
|
||||
'_blank',
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ChartCard
|
||||
title={t('chart.issue-status-count.title')}
|
||||
description={t('chart.issue-status-count.description')}
|
||||
extra={
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={type}
|
||||
onValueChange={onChangeType('pie')}
|
||||
>
|
||||
<ToggleGroupItem value="bar">Bar</ToggleGroupItem>
|
||||
<ToggleGroupItem value="pie">Pie</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
}
|
||||
>
|
||||
{type === 'pie' && (
|
||||
<div className="flex items-center">
|
||||
<PieChart data={data} onClick={onClickIssue} height={415} />
|
||||
<IssueLegend data={data} />
|
||||
</div>
|
||||
)}
|
||||
{type === 'bar' && (
|
||||
<BarChart data={data} height={415} onClick={onClickIssue} />
|
||||
)}
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
interface IssueLegendProps {
|
||||
data?: { name: string; value: number; color: string }[];
|
||||
}
|
||||
|
||||
const IssueLegend: React.FC<IssueLegendProps> = ({ data }) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="table-head !text-large-strong text-neutral-primary h-auto">
|
||||
Status
|
||||
</th>
|
||||
<th className="table-head !text-large-strong text-neutral-primary h-auto">
|
||||
Issue Count
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.map((item, index) => (
|
||||
<tr key={`item-${index}`}>
|
||||
<td className="!text-large-normal text-neutral-primary table-cell py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="!text-large-normal text-neutral-primary table-cell py-2">
|
||||
{item.value}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IssueBarChart;
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useThrottle } from 'react-use';
|
||||
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxGroup,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxTrigger,
|
||||
Icon,
|
||||
toast,
|
||||
} from '@ufb/react';
|
||||
|
||||
import {
|
||||
ChartCard,
|
||||
client,
|
||||
commandFilter,
|
||||
getDayCount,
|
||||
InfiniteScrollArea,
|
||||
Legend,
|
||||
LineChart,
|
||||
useOAIQuery,
|
||||
} from '@/shared';
|
||||
import type { Issue } from '@/entities/issue';
|
||||
import { IssueBadge, useIssueSearchInfinite } from '@/entities/issue';
|
||||
|
||||
import { useLineChartData } from '../lib';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
from: Date;
|
||||
to: Date;
|
||||
}
|
||||
|
||||
const IssueFeedbackLineChart: React.FC<IProps> = ({ from, projectId, to }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [searchName, setSearchName] = useState('');
|
||||
const throttledSearchName = useThrottle(searchName, 1000);
|
||||
const [currentIssues, setCurrentIssues] = useState<Issue[]>([]);
|
||||
|
||||
const { data } = useOAIQuery({
|
||||
path: '/api/admin/statistics/feedback-issue',
|
||||
variables: {
|
||||
startDate: dayjs(from).startOf('day').format('YYYY-MM-DD'),
|
||||
endDate: dayjs(to).endOf('day').format('YYYY-MM-DD'),
|
||||
issueIds: currentIssues.map((issue) => issue.id).join(','),
|
||||
interval: getDayCount(from, to) > 50 ? 'week' : 'day',
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
void client
|
||||
.post({
|
||||
path: '/api/admin/projects/{projectId}/issues/search',
|
||||
body: { sort: { feedbackCount: 'DESC' }, limit: 5 },
|
||||
pathParams: { projectId },
|
||||
})
|
||||
.then(({ data }) => setCurrentIssues(data?.items ?? []));
|
||||
}, [projectId]);
|
||||
|
||||
const { chartData, dataKeys } = useLineChartData(
|
||||
from,
|
||||
to,
|
||||
currentIssues,
|
||||
data?.issues.map((v) => ({
|
||||
id: v.id,
|
||||
statistics: v.statistics.map(({ startDate, endDate, feedbackCount }) => ({
|
||||
startDate,
|
||||
endDate,
|
||||
count: feedbackCount,
|
||||
})),
|
||||
})),
|
||||
);
|
||||
|
||||
const {
|
||||
data: allIssueData,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useIssueSearchInfinite(Number(projectId), {
|
||||
limit: 10,
|
||||
queries: [
|
||||
{ key: 'name', value: throttledSearchName, condition: 'CONTAINS' },
|
||||
],
|
||||
sort: { name: 'ASC' },
|
||||
});
|
||||
|
||||
const allIssues = useMemo(() => {
|
||||
return allIssueData.pages
|
||||
.map((v) => v?.items)
|
||||
.filter((v) => !!v)
|
||||
.flat();
|
||||
}, [allIssueData]);
|
||||
|
||||
const handleIssueCheck = (issue: Issue) => {
|
||||
if (currentIssues.length === 5) {
|
||||
toast.error(t('popover.select-issue.max-issue-count'));
|
||||
return;
|
||||
}
|
||||
setCurrentIssues((prev) => [...prev, issue]);
|
||||
setSearchName('');
|
||||
};
|
||||
const handleIssueUncheck = (issue: Issue) => {
|
||||
setCurrentIssues((prev) => prev.filter((v) => v.id !== issue.id));
|
||||
};
|
||||
|
||||
return (
|
||||
<ChartCard
|
||||
title={t('chart.issue-comparison.title')}
|
||||
description={`${t('chart.issue-comparison.description')} (${dayjs(
|
||||
from,
|
||||
).format('YYYY/MM/DD')} - ${dayjs(to).format('YYYY/MM/DD')})`}
|
||||
extra={
|
||||
<div className="flex gap-3">
|
||||
<Legend dataKeys={dataKeys} />
|
||||
<Combobox>
|
||||
<ComboboxTrigger>
|
||||
<Icon name="RiFilter3Line" />
|
||||
Filter
|
||||
</ComboboxTrigger>
|
||||
<ComboboxContent options={{ filter: commandFilter }}>
|
||||
<ComboboxInput
|
||||
onValueChange={(value) => setSearchName(value)}
|
||||
value={searchName}
|
||||
/>
|
||||
<ComboboxList maxHeight="200px">
|
||||
<ComboboxEmpty>No results found.</ComboboxEmpty>
|
||||
<ComboboxGroup
|
||||
heading={
|
||||
<span className="text-neutral-tertiary text-base-normal">
|
||||
Selected Issue
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{currentIssues.map((issue) => (
|
||||
<ComboboxItem
|
||||
key={issue.id}
|
||||
onSelect={() => handleIssueUncheck(issue)}
|
||||
>
|
||||
<IssueBadge name={issue.name} status={issue.status} />
|
||||
</ComboboxItem>
|
||||
))}
|
||||
</ComboboxGroup>
|
||||
{allIssues.length > 0 && (
|
||||
<ComboboxGroup
|
||||
heading={
|
||||
<span className="text-neutral-tertiary text-base-normal">
|
||||
Issue List
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{allIssues
|
||||
.filter(
|
||||
(v) =>
|
||||
!currentIssues.some((issue) => issue.id === v.id),
|
||||
)
|
||||
.map((issue) => (
|
||||
<ComboboxItem
|
||||
key={issue.id}
|
||||
value={issue.name}
|
||||
onSelect={() => handleIssueCheck(issue)}
|
||||
>
|
||||
<IssueBadge name={issue.name} status={issue.status} />
|
||||
</ComboboxItem>
|
||||
))}
|
||||
<InfiniteScrollArea
|
||||
fetchNextPage={fetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
/>
|
||||
</ComboboxGroup>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LineChart height={400} dataKeys={dataKeys} data={chartData} />
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default IssueFeedbackLineChart;
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { ChartCard, getDayCount, LineChart, useOAIQuery } from '@/shared';
|
||||
|
||||
import { useLineChartData } from '../lib';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
from: Date;
|
||||
to: Date;
|
||||
}
|
||||
|
||||
const IssueLineChart: React.FC<IProps> = ({ from, projectId, to }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data } = useOAIQuery({
|
||||
path: '/api/admin/statistics/issue/count-by-date',
|
||||
variables: {
|
||||
projectId,
|
||||
startDate: dayjs(from).startOf('day').format('YYYY-MM-DD'),
|
||||
endDate: dayjs(to).endOf('day').format('YYYY-MM-DD'),
|
||||
interval: getDayCount(from, to) > 50 ? 'week' : 'day',
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { chartData, dataKeys } = useLineChartData(
|
||||
from,
|
||||
to,
|
||||
[{ id: 1, name: '' }],
|
||||
[{ id: 1, statistics: data?.statistics ?? [] }],
|
||||
);
|
||||
|
||||
return (
|
||||
<ChartCard
|
||||
title={t('chart.issue-trend.title')}
|
||||
description={`${t('chart.issue-trend.description')} (${dayjs(from).format(
|
||||
'YYYY/MM/DD',
|
||||
)} - ${dayjs(to).format('YYYY/MM/DD')})`}
|
||||
>
|
||||
<LineChart height={400} data={chartData} dataKeys={dataKeys} />
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default IssueLineChart;
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
createColumnHelper,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxContent,
|
||||
ComboboxList,
|
||||
ComboboxSelectItem,
|
||||
ComboboxTrigger,
|
||||
Icon,
|
||||
} from '@ufb/react';
|
||||
|
||||
import { ISSUES, Path } from '@/shared';
|
||||
import { DashboardTable } from '@/shared/ui';
|
||||
import type { IssueStatus } from '@/entities/issue';
|
||||
import { IssueBadge, useIssueSearch } from '@/entities/issue';
|
||||
|
||||
interface IssueTableData {
|
||||
id: number;
|
||||
no: number;
|
||||
status: IssueStatus;
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<IssueTableData>();
|
||||
const columns = [
|
||||
columnHelper.accessor('no', {
|
||||
header: 'No',
|
||||
enableSorting: false,
|
||||
size: 30,
|
||||
}),
|
||||
columnHelper.accessor('name', {
|
||||
header: 'Issue',
|
||||
enableSorting: false,
|
||||
cell({ getValue, row }) {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<>
|
||||
<span>{getValue()}</span>
|
||||
<Link
|
||||
href={{
|
||||
pathname: Path.ISSUE,
|
||||
query: {
|
||||
projectId: router.query.projectId,
|
||||
queries: JSON.stringify([
|
||||
{ key: 'name', value: row.original.name, condition: 'IS' },
|
||||
]),
|
||||
},
|
||||
}}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="ml-1"
|
||||
>
|
||||
<Icon
|
||||
name="RiExternalLinkFill"
|
||||
size={16}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('status', {
|
||||
header: 'Status',
|
||||
enableSorting: false,
|
||||
cell({ getValue }) {
|
||||
return <IssueBadge status={getValue()} />;
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('count', {
|
||||
header: 'Feedback Count',
|
||||
cell: ({ getValue }) => <p>{getValue().toLocaleString()}</p>,
|
||||
enableSorting: true,
|
||||
}),
|
||||
];
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
from: Date;
|
||||
to: Date;
|
||||
}
|
||||
const limitOptions = [
|
||||
{ label: 'Top 5', value: '5' },
|
||||
{ label: 'Top 10', value: '10' },
|
||||
{ label: 'Top 15', value: '15' },
|
||||
{ label: 'Top 20', value: '20' },
|
||||
];
|
||||
|
||||
const IssueRank: React.FC<IProps> = ({ projectId }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const issues = useMemo(() => ISSUES(t), [t]);
|
||||
const [limit, setLimit] = useState(Number(limitOptions[0]?.value ?? 0));
|
||||
|
||||
const [currentIssueStatusList, setCurrentIssueStatusList] = useState(issues);
|
||||
|
||||
const { data } = useIssueSearch(projectId, {
|
||||
sort: { feedbackCount: 'DESC' },
|
||||
limit,
|
||||
queries: currentIssueStatusList.map((v) => ({
|
||||
key: 'status',
|
||||
value: v.key,
|
||||
condition: 'IS',
|
||||
})),
|
||||
operator: 'OR',
|
||||
});
|
||||
|
||||
const newData = useMemo(
|
||||
() =>
|
||||
data?.items.map((item, i) => ({
|
||||
id: item.id,
|
||||
no: i + 1,
|
||||
count: item.feedbackCount,
|
||||
name: item.name,
|
||||
status: item.status,
|
||||
})) ?? [],
|
||||
[data, t],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
columns,
|
||||
data: newData,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<DashboardTable
|
||||
title={t('chart.issue-rank.title')}
|
||||
description={t('chart.issue-rank.description')}
|
||||
table={table}
|
||||
selectData={{
|
||||
options: limitOptions,
|
||||
value: String(limit),
|
||||
onChange: (v) => setLimit(Number(v)),
|
||||
}}
|
||||
filterContent={
|
||||
<Combobox>
|
||||
<ComboboxTrigger>
|
||||
<Icon name="RiFilter3Line" />
|
||||
Filter
|
||||
</ComboboxTrigger>
|
||||
<ComboboxContent>
|
||||
<ComboboxList>
|
||||
{issues.map((issue) => (
|
||||
<ComboboxSelectItem
|
||||
key={issue.key}
|
||||
checked={currentIssueStatusList.some(
|
||||
({ key }) => key === issue.key,
|
||||
)}
|
||||
onSelect={() => {
|
||||
const isChecked = currentIssueStatusList.some(
|
||||
({ key }) => key === issue.key,
|
||||
);
|
||||
setCurrentIssueStatusList((prev) =>
|
||||
isChecked ?
|
||||
prev.filter((v) => v.key !== issue.key)
|
||||
: [...prev, issue],
|
||||
);
|
||||
}}
|
||||
>
|
||||
{issue.name}
|
||||
</ComboboxSelectItem>
|
||||
))}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default IssueRank;
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { DashboardCard, useOAIQuery } from '@/shared';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
const SevenDaysFeedbackCard: React.FC<IProps> = ({ projectId }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: currentData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/feedback/count',
|
||||
variables: {
|
||||
from: dayjs().subtract(7, 'day').startOf('day').toISOString(),
|
||||
to: dayjs().subtract(1, 'day').endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: previousData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/feedback/count',
|
||||
variables: {
|
||||
from: dayjs().subtract(14, 'day').startOf('day').toISOString(),
|
||||
to: dayjs().subtract(8, 'day').endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
const percentage = useMemo(() => {
|
||||
if (!currentData || !previousData) return 0;
|
||||
return (
|
||||
((currentData.count - previousData.count) / previousData.count) * 100
|
||||
);
|
||||
}, [currentData, previousData]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
type="feedback"
|
||||
data={currentData?.count ?? 0}
|
||||
title={t('dashboard-card.n-days-feedback.title', { n: 7 })}
|
||||
description={t('dashboard-card.n-days-feedback.description', {
|
||||
n: 7,
|
||||
targetDate: `${dayjs()
|
||||
.subtract(7, 'day')
|
||||
.format('YYYY/MM/DD')} - ${dayjs()
|
||||
.subtract(1, 'day')
|
||||
.format('YYYY/MM/DD')}`,
|
||||
compareDate: `${dayjs()
|
||||
.subtract(14, 'day')
|
||||
.format('YYYY/MM/DD')} - ${dayjs()
|
||||
.subtract(8, 'day')
|
||||
.format('YYYY/MM/DD')}`,
|
||||
})}
|
||||
percentage={percentage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SevenDaysFeedbackCard;
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { DashboardCard, useOAIQuery } from '@/shared';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
const SevenDaysIssueCard: React.FC<IProps> = ({ projectId }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: currentData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/issue/count',
|
||||
variables: {
|
||||
from: dayjs().subtract(7, 'day').startOf('day').toISOString(),
|
||||
to: dayjs().subtract(1, 'day').endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
const { data: previousData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/issue/count',
|
||||
variables: {
|
||||
from: dayjs().subtract(14, 'day').startOf('day').toISOString(),
|
||||
to: dayjs().subtract(8, 'day').endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
const percentage = useMemo(() => {
|
||||
if (!currentData || !previousData) return 0;
|
||||
return (
|
||||
((currentData.count - previousData.count) / previousData.count) * 100
|
||||
);
|
||||
}, [currentData, previousData]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
type="issue"
|
||||
data={currentData?.count ?? 0}
|
||||
title={t('dashboard-card.n-days-issue.title', { n: 7 })}
|
||||
description={t('dashboard-card.n-days-issue.description', {
|
||||
n: 7,
|
||||
targetDate: `${dayjs()
|
||||
.subtract(7, 'day')
|
||||
.format('YYYY/MM/DD')} - ${dayjs()
|
||||
.subtract(1, 'day')
|
||||
.format('YYYY/MM/DD')}`,
|
||||
compareDate: `${dayjs()
|
||||
.subtract(14, 'day')
|
||||
.format('YYYY/MM/DD')} - ${dayjs()
|
||||
.subtract(8, 'day')
|
||||
.format('YYYY/MM/DD')}`,
|
||||
})}
|
||||
percentage={percentage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SevenDaysIssueCard;
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { DashboardCard, useOAIQuery } from '@/shared';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
const ThirtyDaysFeedbackCard: React.FC<IProps> = ({ projectId }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: currentData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/feedback/count',
|
||||
variables: {
|
||||
from: dayjs().subtract(30, 'day').startOf('day').toISOString(),
|
||||
to: dayjs().subtract(1, 'day').endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
const { data: previousData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/feedback/count',
|
||||
variables: {
|
||||
from: dayjs().subtract(60, 'day').startOf('day').toISOString(),
|
||||
to: dayjs().subtract(31, 'day').endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
const percentage = useMemo(() => {
|
||||
if (!currentData || !previousData) return 0;
|
||||
return (
|
||||
((currentData.count - previousData.count) / previousData.count) * 100
|
||||
);
|
||||
}, [currentData, previousData]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
type="feedback"
|
||||
data={currentData?.count ?? 0}
|
||||
title={t('dashboard-card.n-days-feedback.title', { n: 30 })}
|
||||
description={t('dashboard-card.n-days-feedback.description', {
|
||||
n: 30,
|
||||
targetDate: `${dayjs()
|
||||
.subtract(30, 'day')
|
||||
.format('YYYY/MM/DD')} - ${dayjs()
|
||||
.subtract(1, 'day')
|
||||
.format('YYYY/MM/DD')}`,
|
||||
compareDate: `${dayjs()
|
||||
.subtract(60, 'day')
|
||||
.format('YYYY/MM/DD')} - ${dayjs()
|
||||
.subtract(31, 'day')
|
||||
.format('YYYY/MM/DD')}`,
|
||||
})}
|
||||
percentage={percentage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThirtyDaysFeedbackCard;
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { DashboardCard, useOAIQuery } from '@/shared';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
const ThirtyDaysIssueCard: React.FC<IProps> = ({ projectId }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: currentData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/issue/count',
|
||||
variables: {
|
||||
from: dayjs().subtract(30, 'day').startOf('day').toISOString(),
|
||||
to: dayjs().subtract(1, 'day').endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: previousData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/issue/count',
|
||||
variables: {
|
||||
from: dayjs().subtract(60, 'day').startOf('day').toISOString(),
|
||||
to: dayjs().subtract(31, 'day').endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
const percentage = useMemo(() => {
|
||||
if (!currentData || !previousData) return 0;
|
||||
|
||||
return (
|
||||
((currentData.count - previousData.count) / previousData.count) * 100
|
||||
);
|
||||
}, [currentData, previousData]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
type="issue"
|
||||
data={currentData?.count ?? 0}
|
||||
title={t('dashboard-card.n-days-issue.title', { n: 30 })}
|
||||
description={t('dashboard-card.n-days-issue.description', {
|
||||
n: 30,
|
||||
targetDate: `${dayjs()
|
||||
.subtract(30, 'day')
|
||||
.format('YYYY/MM/DD')} - ${dayjs()
|
||||
.subtract(1, 'day')
|
||||
.format('YYYY/MM/DD')}`,
|
||||
compareDate: `${dayjs()
|
||||
.subtract(60, 'day')
|
||||
.format('YYYY/MM/DD')} - ${dayjs()
|
||||
.subtract(31, 'day')
|
||||
.format('YYYY/MM/DD')}`,
|
||||
})}
|
||||
percentage={percentage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThirtyDaysIssueCard;
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { DashboardCard, useOAIQuery } from '@/shared';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
const TodayFeedbackCard: React.FC<IProps> = ({ projectId }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: currentData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/feedback/count',
|
||||
variables: {
|
||||
from: dayjs().startOf('day').toISOString(),
|
||||
to: dayjs().endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: previousData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/feedback/count',
|
||||
variables: {
|
||||
from: dayjs().subtract(1, 'day').startOf('day').toISOString(),
|
||||
to: dayjs().subtract(1, 'day').endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
const percentage = useMemo(() => {
|
||||
if (!currentData || !previousData) return 0;
|
||||
return (
|
||||
((currentData.count - previousData.count) / previousData.count) * 100
|
||||
);
|
||||
}, [currentData, previousData]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
type="feedback"
|
||||
data={(currentData?.count ?? 0).toLocaleString()}
|
||||
title={t('dashboard-card.today-feedback.title')}
|
||||
description={t('dashboard-card.today-feedback.description', {
|
||||
targetDate: dayjs().format('YYYY/MM/DD'),
|
||||
compareDate: dayjs().subtract(1, 'day').format('YYYY/MM/DD'),
|
||||
})}
|
||||
percentage={percentage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TodayFeedbackCard;
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { DashboardCard, useOAIQuery } from '@/shared';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
const TodayIssueCard: React.FC<IProps> = ({ projectId }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: currentData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/issue/count',
|
||||
variables: {
|
||||
from: dayjs().startOf('day').toISOString(),
|
||||
to: dayjs().endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: previousData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/issue/count',
|
||||
variables: {
|
||||
from: dayjs().subtract(1, 'day').startOf('day').toISOString(),
|
||||
to: dayjs().subtract(1, 'day').endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
const percentage = useMemo(() => {
|
||||
if (!currentData || !previousData) return 0;
|
||||
return (
|
||||
((currentData.count - previousData.count) / previousData.count) * 100
|
||||
);
|
||||
}, [currentData, previousData]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
type="issue"
|
||||
data={currentData?.count ?? 0}
|
||||
title={t('dashboard-card.today-issue.title')}
|
||||
description={t('dashboard-card.today-issue.description', {
|
||||
targetDate: dayjs().format('YYYY/MM/DD'),
|
||||
compareDate: dayjs().subtract(1, 'day').format('YYYY/MM/DD'),
|
||||
})}
|
||||
percentage={percentage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TodayIssueCard;
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { DashboardCard, useOAIQuery } from '@/shared';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
from: Date;
|
||||
to: Date;
|
||||
}
|
||||
|
||||
const TotalFeedbackCard: React.FC<IProps> = ({ projectId, from, to }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data } = useOAIQuery({
|
||||
path: '/api/admin/statistics/feedback/count',
|
||||
variables: {
|
||||
from: dayjs(from).startOf('day').toISOString(),
|
||||
to: dayjs(to).endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
type="feedback"
|
||||
data={data?.count ?? 0}
|
||||
title={t('dashboard-card.total-feedback.title')}
|
||||
description={t('dashboard-card.total-feedback.description', {
|
||||
targetDate: `${dayjs(from).format('YYYY/MM/DD')} - ${dayjs(to).format(
|
||||
'YYYY/MM/DD',
|
||||
)}`,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TotalFeedbackCard;
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { DashboardCard, useOAIQuery } from '@/shared';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
from: Date;
|
||||
to: Date;
|
||||
}
|
||||
|
||||
const TotalIssueCard: React.FC<IProps> = ({ from, to, projectId }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data } = useOAIQuery({
|
||||
path: '/api/admin/statistics/issue/count',
|
||||
variables: {
|
||||
from: dayjs(from).startOf('day').toISOString(),
|
||||
to: dayjs(to).endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
type="issue"
|
||||
data={data?.count ?? 0}
|
||||
title={t('dashboard-card.total-issue.title')}
|
||||
description={t('dashboard-card.total-issue.description', {
|
||||
targetDate: `${dayjs(from).format('YYYY/MM/DD')} - ${dayjs(to).format(
|
||||
'YYYY/MM/DD',
|
||||
)}`,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TotalIssueCard;
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { DashboardCard, useOAIQuery } from '@/shared';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
const YesterdayFeedbackCard: React.FC<IProps> = ({ projectId }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: currentData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/feedback/count',
|
||||
variables: {
|
||||
from: dayjs().subtract(1, 'day').startOf('day').toISOString(),
|
||||
to: dayjs().subtract(1, 'day').endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: previousData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/feedback/count',
|
||||
variables: {
|
||||
from: dayjs().subtract(2, 'day').startOf('day').toISOString(),
|
||||
to: dayjs().subtract(2, 'day').endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
const percentage = useMemo(() => {
|
||||
if (!currentData || !previousData) return 0;
|
||||
return (
|
||||
((currentData.count - previousData.count) / previousData.count) * 100
|
||||
);
|
||||
}, [currentData, previousData]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
type="feedback"
|
||||
data={currentData?.count ?? 0}
|
||||
title={t('dashboard-card.yesterday-feedback.title')}
|
||||
description={t('dashboard-card.yesterday-feedback.description', {
|
||||
targetDate: dayjs().subtract(1, 'day').format('YYYY/MM/DD'),
|
||||
compareDate: dayjs().subtract(2, 'day').format('YYYY/MM/DD'),
|
||||
})}
|
||||
percentage={percentage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default YesterdayFeedbackCard;
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { DashboardCard, useOAIQuery } from '@/shared';
|
||||
|
||||
interface IProps {
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
const YesterdayIssueCard: React.FC<IProps> = ({ projectId }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: currentData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/issue/count',
|
||||
variables: {
|
||||
from: dayjs().subtract(1, 'day').startOf('day').toISOString(),
|
||||
to: dayjs().subtract(1, 'day').endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
const { data: previousData } = useOAIQuery({
|
||||
path: '/api/admin/statistics/issue/count',
|
||||
variables: {
|
||||
from: dayjs().subtract(2, 'day').startOf('day').toISOString(),
|
||||
to: dayjs().subtract(2, 'day').endOf('day').toISOString(),
|
||||
projectId,
|
||||
},
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: false,
|
||||
},
|
||||
});
|
||||
|
||||
const percentage = useMemo(() => {
|
||||
if (!currentData || !previousData) return 0;
|
||||
return (
|
||||
((currentData.count - previousData.count) / previousData.count) * 100
|
||||
);
|
||||
}, [currentData, previousData]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
type="issue"
|
||||
data={currentData?.count ?? 0}
|
||||
title={t('dashboard-card.yesterday-issue.title')}
|
||||
description={t('dashboard-card.yesterday-issue.description', {
|
||||
targetDate: dayjs().subtract(1, 'day').format('YYYY/MM/DD'),
|
||||
compareDate: dayjs().subtract(2, 'day').format('YYYY/MM/DD'),
|
||||
})}
|
||||
percentage={percentage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default YesterdayIssueCard;
|
||||
Reference in new issue
Block a user