feat!: первая готовая версия веб интерфейса

This commit is contained in:
2025-05-27 22:24:05 +04:00
parent 6b1f57a3b9
commit 9401c42ffd
6 changed files with 284 additions and 96 deletions

View File

@@ -243,13 +243,13 @@ public class ReportController(IReportAdapter adapter) : ControllerBase
/// Отправка pdf отчета Вкладов и Кредитных программ по Валютам
/// кладовщик pdf
/// </summary>
/// <param name="email"></param>
/// <param name="mailInfo"></param>
/// <param name="fromDate"></param>
/// <param name="toDate"></param>
/// <param name="ct"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> SendReportByCurrency(string email, DateTime fromDate, DateTime toDate, CancellationToken ct)
public async Task<IActionResult> SendReportByCurrency([FromBody] DepositReportMailSendInfoBindingModel mailInfo, DateTime fromDate, DateTime toDate, CancellationToken ct)
{
try
{
@@ -267,9 +267,9 @@ public class ReportController(IReportAdapter adapter) : ControllerBase
}
await _emailService.SendReportAsync(
toEmail: email,
subject: "Отчет по вкладам и кредитным программам по валютам",
body: $"<h1>Отчет по вкладам и кредитным программам по валютам</h1><p>Отчет за период с {fromDate:dd.MM.yyyy} по {toDate:dd.MM.yyyy}</p>",
toEmail: mailInfo.Email,
subject: mailInfo.Subject,
body: mailInfo.Body,
attachmentPath: tempPathWithExtension
);
@@ -333,16 +333,15 @@ public class ReportController(IReportAdapter adapter) : ControllerBase
/// <summary>
/// Отправка word отчета Вкладов по Кредитных программ
/// </summary>
/// <param name="email"></param>
/// <param name="creditProgramIds"></param>
/// <param name="mailInfo"></param>
/// <param name="ct"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> SendReportDepositByCreditProgram(string email, [FromQuery] List<string>? creditProgramIds, CancellationToken ct)
public async Task<IActionResult> SendReportDepositByCreditProgram([FromBody] CreditProgramReportMailSendInfoBindingModel mailInfo, CancellationToken ct)
{
try
{
var report = await _adapter.CreateDocumentDepositByCreditProgramAsync(creditProgramIds, ct);
var report = await _adapter.CreateDocumentDepositByCreditProgramAsync(mailInfo.CreditProgramIds, ct);
var response = report.GetResponse(Request, Response);
if (response is FileStreamResult fileResult)
@@ -356,9 +355,9 @@ public class ReportController(IReportAdapter adapter) : ControllerBase
}
await _emailService.SendReportAsync(
toEmail: email,
subject: "Отчет по вкладам по кредитным программам",
body: "<h1>Отчет по вкладам по кредитным программам</h1><p>В приложении находится отчет по вкладам по кредитным программам.</p>",
toEmail: mailInfo.Email,
subject: mailInfo.Subject,
body: mailInfo.Body,
attachmentPath: tempPathWithExtension
);
@@ -378,16 +377,15 @@ public class ReportController(IReportAdapter adapter) : ControllerBase
/// <summary>
/// Отправка excel отчета Вкладов по Кредитных программ
/// </summary>
/// <param name="email"></param>
/// <param name="creditProgramIds"></param>
/// <param name="mailInfo"></param>
/// <param name="ct"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> SendExcelReportDepositByCreditProgram(string email, [FromQuery] List<string>? creditProgramIds, CancellationToken ct)
public async Task<IActionResult> SendExcelReportDepositByCreditProgram([FromBody] CreditProgramReportMailSendInfoBindingModel mailInfo, CancellationToken ct)
{
try
{
var report = await _adapter.CreateExcelDocumentDepositByCreditProgramAsync(creditProgramIds, ct);
var report = await _adapter.CreateExcelDocumentDepositByCreditProgramAsync(mailInfo.CreditProgramIds, ct);
var response = report.GetResponse(Request, Response);
if (response is FileStreamResult fileResult)
@@ -401,9 +399,9 @@ public class ReportController(IReportAdapter adapter) : ControllerBase
}
await _emailService.SendReportAsync(
toEmail: email,
subject: "Excel отчет по вкладам по кредитным программам",
body: "<h1>Excel отчет по вкладам по кредитным программам</h1><p>В приложении находится Excel отчет по вкладам по кредитным программам.</p>",
toEmail: mailInfo.Email,
subject: mailInfo.Subject,
body: mailInfo.Body,
attachmentPath: tempPathWithExtension
);

View File

@@ -1,12 +1,11 @@
import { ConfigManager } from '@/lib/config';
import type {
CreditProgramReportMailSendInfoBindingModel,
DepositReportMailSendInfoBindingModel,
} from '@/types/types';
const API_URL = ConfigManager.loadUrl();
interface SendEmailRequest {
toEmail: string;
creditProgramIds?: string[];
}
export const reportsApi = {
// PDF отчеты
getPdfReport: async (fromDate: string, toDate: string) => {
@@ -26,7 +25,7 @@ export const reportsApi = {
},
sendPdfReportByEmail: async (
toEmail: string,
mailInfo: DepositReportMailSendInfoBindingModel,
fromDate: string,
toDate: string,
) => {
@@ -38,9 +37,7 @@ export const reportsApi = {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
email: toEmail,
}),
body: JSON.stringify(mailInfo),
},
);
if (!res.ok) {
@@ -69,7 +66,9 @@ export const reportsApi = {
return res.blob();
},
sendWordReportByEmail: async (request: SendEmailRequest) => {
sendWordReportByEmail: async (
mailInfo: CreditProgramReportMailSendInfoBindingModel,
) => {
const res = await fetch(
`${API_URL}/api/Report/SendReportDepositByCreditProgram`,
{
@@ -78,10 +77,7 @@ export const reportsApi = {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
email: request.toEmail,
creditProgramIds: request.creditProgramIds,
}),
body: JSON.stringify(mailInfo),
},
);
if (!res.ok) {
@@ -109,7 +105,9 @@ export const reportsApi = {
return res.blob();
},
sendExcelReportByEmail: async (request: SendEmailRequest) => {
sendExcelReportByEmail: async (
mailInfo: CreditProgramReportMailSendInfoBindingModel,
) => {
const res = await fetch(
`${API_URL}/api/Report/SendExcelReportDepositByCreditProgram`,
{
@@ -118,10 +116,7 @@ export const reportsApi = {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
email: request.toEmail,
creditProgramIds: request.creditProgramIds,
}),
body: JSON.stringify(mailInfo),
},
);
if (!res.ok) {

View File

@@ -4,15 +4,11 @@ import { ReportViewer } from '../features/ReportViewer';
import { reportsApi } from '@/api/reports';
import { format } from 'date-fns';
import { toast } from 'sonner';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { EmailDialog } from '@/components/ui/EmailDialog';
import type {
CreditProgramReportMailSendInfoBindingModel,
DepositReportMailSendInfoBindingModel,
} from '@/types/types';
type ReportCategory = 'pdf' | 'word-excel' | null;
type FileFormat = 'doc' | 'xls';
@@ -26,7 +22,7 @@ export const Reports = (): React.JSX.Element => {
mimeType: string;
} | null>(null);
const [isEmailDialogOpen, setIsEmailDialogOpen] = React.useState(false);
const [email, setEmail] = React.useState('');
const [isEmailLoading, setIsEmailLoading] = React.useState(false);
const [pendingEmailAction, setPendingEmailAction] = React.useState<
| {
type: 'pdf';
@@ -125,48 +121,58 @@ export const Reports = (): React.JSX.Element => {
setIsEmailDialogOpen(true);
};
const handleEmailSubmit = async () => {
if (!email.trim()) {
toast.error('Пожалуйста, введите email');
return;
}
const handleEmailSubmit = async (emailData: {
email: string;
subject: string;
body: string;
}) => {
if (!pendingEmailAction) {
toast.error('Нет данных для отправки');
return;
}
setIsEmailLoading(true);
try {
if (pendingEmailAction.type === 'pdf') {
const { fromDate, toDate } = pendingEmailAction.data;
const mailInfo: DepositReportMailSendInfoBindingModel = {
email: emailData.email,
toEmail: emailData.email,
subject: emailData.subject,
body: emailData.body,
fromDate: format(fromDate, 'yyyy-MM-dd'),
toDate: format(toDate, 'yyyy-MM-dd'),
};
await reportsApi.sendPdfReportByEmail(
email,
mailInfo,
format(fromDate, 'yyyy-MM-dd'),
format(toDate, 'yyyy-MM-dd'),
);
} else {
const { format: fileFormat, creditProgramIds } =
pendingEmailAction.data;
const mailInfo: CreditProgramReportMailSendInfoBindingModel = {
email: emailData.email,
toEmail: emailData.email,
subject: emailData.subject,
body: emailData.body,
creditProgramIds,
};
if (fileFormat === 'doc') {
await reportsApi.sendWordReportByEmail({
toEmail: email,
creditProgramIds,
});
await reportsApi.sendWordReportByEmail(mailInfo);
} else {
await reportsApi.sendExcelReportByEmail({
toEmail: email,
creditProgramIds,
});
await reportsApi.sendExcelReportByEmail(mailInfo);
}
}
toast.success('Отчет успешно отправлен на email');
setIsEmailDialogOpen(false);
setEmail('');
setPendingEmailAction(null);
} catch (error) {
toast.error('Ошибка при отправке отчета на email');
console.error(error);
} finally {
setIsEmailLoading(false);
}
};
@@ -197,38 +203,35 @@ export const Reports = (): React.JSX.Element => {
/>
</div>
<Dialog open={isEmailDialogOpen} onOpenChange={setIsEmailDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Отправка отчета на email</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="email">Email адрес</Label>
<Input
id="email"
type="email"
placeholder="example@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => {
setIsEmailDialogOpen(false);
setEmail('');
setPendingEmailAction(null);
}}
>
Отмена
</Button>
<Button onClick={handleEmailSubmit}>Отправить</Button>
</div>
</div>
</DialogContent>
</Dialog>
<EmailDialog
isOpen={isEmailDialogOpen}
onClose={() => {
setIsEmailDialogOpen(false);
setPendingEmailAction(null);
}}
onSubmit={handleEmailSubmit}
isLoading={isEmailLoading}
defaultSubject={
pendingEmailAction?.type === 'pdf'
? 'Отчет по вкладам и кредитным программам по валютам'
: pendingEmailAction?.data.format === 'doc'
? 'Word отчет по вкладам по кредитным программам'
: 'Excel отчет по вкладам по кредитным программам'
}
defaultBody={
pendingEmailAction?.type === 'pdf'
? `Отчет по вкладам и кредитным программам по валютам за период с ${
pendingEmailAction.data.fromDate
? format(pendingEmailAction.data.fromDate, 'dd.MM.yyyy')
: ''
} по ${
pendingEmailAction.data.toDate
? format(pendingEmailAction.data.toDate, 'dd.MM.yyyy')
: ''
}`
: 'В приложении находится отчет по вкладам по кредитным программам.'
}
/>
</>
);
};

View File

@@ -0,0 +1,152 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
const emailSchema = z.object({
email: z.string().email('Введите корректный email адрес'),
subject: z.string().min(1, 'Тема письма обязательна'),
body: z.string().min(1, 'Текст письма обязателен'),
});
type EmailFormData = z.infer<typeof emailSchema>;
interface EmailDialogProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (data: EmailFormData) => void;
isLoading?: boolean;
defaultSubject?: string;
defaultBody?: string;
}
export const EmailDialog: React.FC<EmailDialogProps> = ({
isOpen,
onClose,
onSubmit,
isLoading = false,
defaultSubject = '',
defaultBody = '',
}) => {
const form = useForm<EmailFormData>({
resolver: zodResolver(emailSchema),
defaultValues: {
email: '',
subject: defaultSubject,
body: defaultBody,
},
});
React.useEffect(() => {
if (isOpen) {
form.reset({
email: '',
subject: defaultSubject,
body: defaultBody,
});
}
}, [isOpen, defaultSubject, defaultBody, form]);
const handleSubmit = (data: EmailFormData) => {
onSubmit(data);
};
const handleClose = () => {
form.reset();
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Отправка отчета на почту</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email адрес</FormLabel>
<FormControl>
<Input
placeholder="example@example.com"
type="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="subject"
render={({ field }) => (
<FormItem>
<FormLabel>Тема письма</FormLabel>
<FormControl>
<Input placeholder="Тема письма" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="body"
render={({ field }) => (
<FormItem>
<FormLabel>Текст письма</FormLabel>
<FormControl>
<Textarea
placeholder="Текст письма..."
className="min-h-[100px]"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={handleClose}
disabled={isLoading}
>
Отмена
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading ? 'Отправка...' : 'Отправить'}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,22 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.TextareaHTMLAttributes<HTMLTextAreaElement>
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
ref={ref}
{...props}
/>
);
});
Textarea.displayName = 'Textarea';
export { Textarea };

View File

@@ -108,3 +108,21 @@ export interface MailSendInfoBindingModel {
body: string;
attachmentPath?: string;
}
export interface ReportMailSendInfoBindingModel
extends MailSendInfoBindingModel {
email: string;
subject: string;
body: string;
}
export interface CreditProgramReportMailSendInfoBindingModel
extends ReportMailSendInfoBindingModel {
creditProgramIds: string[];
}
export interface DepositReportMailSendInfoBindingModel
extends ReportMailSendInfoBindingModel {
fromDate: string;
toDate: string;
}