first commit
CI / lint (push) Failing after 49s
CI / format (push) Failing after 1m6s
CI / typecheck (push) Successful in 1m8s
CI / test (push) Failing after 1m8s

This commit is contained in:
SDI committed 2026-07-15 18:05:12 +09:00
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;