回筐导出
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use App\Models\ContainerReturnModel;
|
||||||
|
use Carbon\CarbonPeriod;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithStrictNullComparison;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 压回筐记录汇总导出:行=日期(区间内逐日),列=门店,
|
||||||
|
* 单元格=当日该门店抵扣(附加)金额合计(同日多条记录合并),无记录填 0;
|
||||||
|
* 末列为行合计,末行为列合计(右下角为总计)
|
||||||
|
*
|
||||||
|
* WithStrictNullComparison:PhpSpreadsheet fromArray 默认松散比较(0 == null 为 true),
|
||||||
|
* 数值 0 的单元格会被跳过写成空白,必须启用严格比较才能把 0 填进表格
|
||||||
|
*/
|
||||||
|
class ContainerReturnExport implements FromCollection, WithStyles, WithStrictNullComparison
|
||||||
|
{
|
||||||
|
private ?Collection $rows = null;
|
||||||
|
|
||||||
|
/** @var array<int, string> 特殊行索引(1 起)=> 类型(title/scope/header/total) */
|
||||||
|
private array $specialRows = [];
|
||||||
|
|
||||||
|
/** 列头所在行索引 */
|
||||||
|
private int $headerRow = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Collection<int, ContainerReturnModel> $records 区间内压回筐记录
|
||||||
|
* @param array<int, string> $storeNames 门店ID => 名称(导出列,升序)
|
||||||
|
* @param string $startDate 开始日期 Y-m-d
|
||||||
|
* @param string $endDate 结束日期 Y-m-d
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly Collection $records,
|
||||||
|
private readonly array $storeNames,
|
||||||
|
private readonly string $startDate,
|
||||||
|
private readonly string $endDate,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出行:标题/范围/列头/日期矩阵/合计全部手工构建(行位置不固定,不用 WithHeadings)
|
||||||
|
*/
|
||||||
|
public function collection(): Collection
|
||||||
|
{
|
||||||
|
if ($this->rows !== null) {
|
||||||
|
return $this->rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同日期同门店多条记录合并:金额代数和(正=压筐附加,负=回筐抵扣)
|
||||||
|
$amounts = [];
|
||||||
|
foreach ($this->records as $record) {
|
||||||
|
$date = $record->created_at->format('Y-m-d');
|
||||||
|
$amounts[$date][(int) $record->store_id] = bcadd(
|
||||||
|
$amounts[$date][(int) $record->store_id] ?? '0',
|
||||||
|
(string) $record->amount,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$rowIndex = 0;
|
||||||
|
|
||||||
|
// 标题行
|
||||||
|
$rows[] = ['压回筐记录汇总(' . $this->startDate . ' ~ ' . $this->endDate . ')'];
|
||||||
|
$this->specialRows[++$rowIndex] = 'title';
|
||||||
|
|
||||||
|
// 范围行:门店 / 导出时间
|
||||||
|
$rows[] = ['门店:' . implode('、', array_values($this->storeNames)) . ' 导出时间:' . now()->format('Y-m-d H:i:s')];
|
||||||
|
$this->specialRows[++$rowIndex] = 'scope';
|
||||||
|
|
||||||
|
// 空行
|
||||||
|
$rows[] = [''];
|
||||||
|
$rowIndex++;
|
||||||
|
|
||||||
|
// 列头:日期 + 门店列 + 行合计
|
||||||
|
$rows[] = array_merge(['日期'], array_values($this->storeNames), ['合计']);
|
||||||
|
$this->specialRows[++$rowIndex] = 'header';
|
||||||
|
$this->headerRow = $rowIndex;
|
||||||
|
|
||||||
|
// 日期矩阵:区间内逐日一行,当天门店无记录填 0
|
||||||
|
$storeIds = array_map(intval(...), array_keys($this->storeNames));
|
||||||
|
$columnTotals = array_fill_keys($storeIds, '0');
|
||||||
|
$grandTotal = '0';
|
||||||
|
foreach (CarbonPeriod::create($this->startDate, $this->endDate) as $date) {
|
||||||
|
$dateKey = $date->format('Y-m-d');
|
||||||
|
$row = [$dateKey];
|
||||||
|
$rowTotal = '0';
|
||||||
|
foreach ($storeIds as $storeId) {
|
||||||
|
$amount = $amounts[$dateKey][$storeId] ?? '0.00';
|
||||||
|
$row[] = (float) $amount;
|
||||||
|
$rowTotal = bcadd($rowTotal, $amount, 2);
|
||||||
|
$columnTotals[$storeId] = bcadd($columnTotals[$storeId], $amount, 2);
|
||||||
|
}
|
||||||
|
$grandTotal = bcadd($grandTotal, $rowTotal, 2);
|
||||||
|
$row[] = (float) $rowTotal;
|
||||||
|
$rows[] = $row;
|
||||||
|
$rowIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合计行:各门店列合计 + 总计
|
||||||
|
$totalRow = ['合计'];
|
||||||
|
foreach ($storeIds as $storeId) {
|
||||||
|
$totalRow[] = (float) $columnTotals[$storeId];
|
||||||
|
}
|
||||||
|
$totalRow[] = (float) $grandTotal;
|
||||||
|
$rows[] = $totalRow;
|
||||||
|
$this->specialRows[++$rowIndex] = 'total';
|
||||||
|
|
||||||
|
return $this->rows = collect($rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标题/列头/合计行加粗,冻结列头行
|
||||||
|
*/
|
||||||
|
public function styles(Worksheet $sheet): array
|
||||||
|
{
|
||||||
|
$this->collection();
|
||||||
|
|
||||||
|
$sheet->freezePane('A' . ($this->headerRow + 1));
|
||||||
|
$sheet->getColumnDimension('A')->setWidth(12);
|
||||||
|
$columnCount = count($this->storeNames) + 2;
|
||||||
|
for ($column = 2; $column <= $columnCount; $column++) {
|
||||||
|
$sheet->getColumnDimensionByColumn($column)->setWidth(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
$styles = [];
|
||||||
|
foreach ($this->specialRows as $row => $type) {
|
||||||
|
$style = match ($type) {
|
||||||
|
'title' => ['font' => ['bold' => true, 'size' => 14]],
|
||||||
|
'header', 'total' => ['font' => ['bold' => true]],
|
||||||
|
default => [],
|
||||||
|
};
|
||||||
|
if ($style !== []) {
|
||||||
|
$styles[$row] = $style;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $styles;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,18 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Recon;
|
namespace App\Http\Controllers\Recon;
|
||||||
|
|
||||||
|
use App\Exceptions\RepositoryException;
|
||||||
|
use App\Exports\ContainerReturnExport;
|
||||||
use App\Models\ContainerReturnModel;
|
use App\Models\ContainerReturnModel;
|
||||||
|
use App\Models\StoreModel;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||||
use Modules\Common\Http\Controllers\BaseController;
|
use Modules\Common\Http\Controllers\BaseController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 压回筐记录(周转筐/托盘跟账单走,生成账单时自动写入完整快照,只读)
|
* 压回筐记录(周转筐/托盘跟账单走,生成账单时自动写入完整快照,只读)
|
||||||
@@ -39,4 +45,61 @@ class ContainerReturnController extends BaseController
|
|||||||
->toArray();
|
->toArray();
|
||||||
return $this->success($data);
|
return $this->success($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 汇总导出:按日期区间 + 门店导出抵扣(附加)金额矩阵,
|
||||||
|
* 行=日期(同日记录合并),列=门店,含行合计/列合计,当天门店无记录填 0
|
||||||
|
*/
|
||||||
|
#[GetRoute(route: '/export', authorize: 'export')]
|
||||||
|
public function export(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'start_date' => 'required|date_format:Y-m-d',
|
||||||
|
'end_date' => 'required|date_format:Y-m-d|after_or_equal:start_date',
|
||||||
|
'store_ids' => 'nullable|string',
|
||||||
|
], [
|
||||||
|
'start_date.required' => '请选择开始日期',
|
||||||
|
'start_date.date_format' => '开始日期格式为 Y-m-d',
|
||||||
|
'end_date.required' => '请选择结束日期',
|
||||||
|
'end_date.date_format' => '结束日期格式为 Y-m-d',
|
||||||
|
'end_date.after_or_equal' => '结束日期不能早于开始日期',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$startDate = $data['start_date'];
|
||||||
|
$endDate = $data['end_date'];
|
||||||
|
if (Carbon::parse($startDate)->diffInDays(Carbon::parse($endDate)) > 366) {
|
||||||
|
throw new RepositoryException('日期区间不能超过 366 天');
|
||||||
|
}
|
||||||
|
|
||||||
|
$storeIds = array_values(array_filter(
|
||||||
|
array_map(intval(...), explode(',', (string) ($data['store_ids'] ?? '')))
|
||||||
|
));
|
||||||
|
|
||||||
|
$records = ContainerReturnModel::query()
|
||||||
|
->whereBetween('created_at', [$startDate . ' 00:00:00', $endDate . ' 23:59:59'])
|
||||||
|
->when($storeIds !== [], static fn ($query) => $query->whereIn('store_id', $storeIds))
|
||||||
|
->get(['id', 'store_id', 'amount', 'created_at']);
|
||||||
|
|
||||||
|
// 已选门店:列取所选门店(无记录填 0);未选门店:列取区间内有记录的门店
|
||||||
|
$columnStoreIds = $storeIds !== []
|
||||||
|
? $storeIds
|
||||||
|
: $records->pluck('store_id')->unique()->map(static fn ($id): int => (int) $id)->all();
|
||||||
|
if ($columnStoreIds === []) {
|
||||||
|
throw new RepositoryException('所选日期区间内无压回筐记录');
|
||||||
|
}
|
||||||
|
sort($columnStoreIds);
|
||||||
|
|
||||||
|
$names = StoreModel::withTrashed()
|
||||||
|
->whereIn('id', $columnStoreIds)
|
||||||
|
->pluck('name', 'id');
|
||||||
|
$storeNames = [];
|
||||||
|
foreach ($columnStoreIds as $storeId) {
|
||||||
|
$storeNames[$storeId] = $names->get($storeId, '门店#' . $storeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Excel::download(
|
||||||
|
new ContainerReturnExport($records, $storeNames, $startDate, $endDate),
|
||||||
|
'回筐记录_' . $startDate . '_' . $endDate . '.xlsx'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ class PermissionSeeder extends Seeder
|
|||||||
'path' => '/recon/container-return',
|
'path' => '/recon/container-return',
|
||||||
'children' => [
|
'children' => [
|
||||||
['type' => 'rule', 'key' => 'recon.containerReturn.query', 'name' => '查询'],
|
['type' => 'rule', 'key' => 'recon.containerReturn.query', 'name' => '查询'],
|
||||||
|
['type' => 'rule', 'key' => 'recon.containerReturn.export', 'name' => '导出'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Exports\ContainerReturnExport;
|
||||||
use App\Models\BillModel;
|
use App\Models\BillModel;
|
||||||
use App\Models\ContainerReturnModel;
|
use App\Models\ContainerReturnModel;
|
||||||
use App\Models\CustomerLevelModel;
|
use App\Models\CustomerLevelModel;
|
||||||
@@ -12,6 +13,7 @@ use App\Models\StoreModel;
|
|||||||
use App\Models\StoreOrderModel;
|
use App\Models\StoreOrderModel;
|
||||||
use App\Models\UserModel;
|
use App\Models\UserModel;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
use Modules\SystemTool\Services\SysSiteConfigService;
|
use Modules\SystemTool\Services\SysSiteConfigService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -182,4 +184,142 @@ class ContainerReturnTest extends ProcurementTestCase
|
|||||||
])->assertJsonPath('success', false);
|
])->assertJsonPath('success', false);
|
||||||
$this->deleteJson('/recon/container-return/1')->assertJsonPath('success', false);
|
$this->deleteJson('/recon/container-return/1')->assertJsonPath('success', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 造一条压回筐记录(直接入库并指定记录时间,用于导出区间/合并断言) */
|
||||||
|
private function makeRecord(StoreModel $store, string $amount, string $createdAt): void
|
||||||
|
{
|
||||||
|
DB::table('container_return')->insert([
|
||||||
|
'store_id' => $store->id,
|
||||||
|
'bill_id' => 0,
|
||||||
|
'box_num' => 0,
|
||||||
|
'tray_num' => 0,
|
||||||
|
'box_price' => '5.00',
|
||||||
|
'tray_price' => '20.00',
|
||||||
|
'amount' => $amount,
|
||||||
|
'operator_id' => 0,
|
||||||
|
'created_at' => $createdAt,
|
||||||
|
'updated_at' => $createdAt,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 汇总导出:行=日期(区间内逐日),列=门店,同日记录合并,无记录填 0,含行/列合计 */
|
||||||
|
public function test_export_matrix_merges_same_day_and_zero_fills(): void
|
||||||
|
{
|
||||||
|
$storeA = StoreModel::factory()->create();
|
||||||
|
$storeB = StoreModel::factory()->create();
|
||||||
|
|
||||||
|
// 08-01:A 两条同日记录合并(+55 压筐、-10 回筐 = 45),B 一条 -20
|
||||||
|
$this->makeRecord($storeA, '55.00', '2026-08-01 09:00:00');
|
||||||
|
$this->makeRecord($storeA, '-10.00', '2026-08-01 15:30:00');
|
||||||
|
$this->makeRecord($storeB, '-20.00', '2026-08-01 10:00:00');
|
||||||
|
// 08-02:仅 A +5,B 无记录填 0
|
||||||
|
$this->makeRecord($storeA, '5.00', '2026-08-02 08:00:00');
|
||||||
|
// 区间外记录不参与
|
||||||
|
$this->makeRecord($storeB, '30.00', '2026-08-04 08:00:00');
|
||||||
|
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
Excel::fake();
|
||||||
|
$this->get('/recon/container-return/export?start_date=2026-08-01&end_date=2026-08-03')->assertOk();
|
||||||
|
|
||||||
|
Excel::assertDownloaded(
|
||||||
|
'回筐记录_2026-08-01_2026-08-03.xlsx',
|
||||||
|
static function (ContainerReturnExport $export) use ($storeA, $storeB): bool {
|
||||||
|
$rows = $export->collection()->values();
|
||||||
|
// 标题1 + 范围1 + 空行1 + 列头1 + 日期3 + 合计1 = 8 行
|
||||||
|
if ($rows->count() !== 8) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 列头:日期 + 门店(未选门店时取区间内有记录的门店,ID 升序)+ 合计
|
||||||
|
if ($rows[3] !== ['日期', $storeA->name, $storeB->name, '合计']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 08-01:同日合并 55-10=45,B -20,行合计 25
|
||||||
|
if ($rows[4] !== ['2026-08-01', 45.0, -20.0, 25.0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 08-02:A 5、B 无记录填 0,行合计 5
|
||||||
|
if ($rows[5] !== ['2026-08-02', 5.0, 0.0, 5.0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 08-03:当日全部无记录,整行填 0
|
||||||
|
if ($rows[6] !== ['2026-08-03', 0.0, 0.0, 0.0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 合计行:A 列 50、B 列 -20、总计 30
|
||||||
|
return $rows[7] === ['合计', 50.0, -20.0, 30.0];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 指定门店导出:仅所选门店成列,未选门店记录忽略;所选门店无记录整列填 0 */
|
||||||
|
public function test_export_filters_by_selected_stores(): void
|
||||||
|
{
|
||||||
|
$storeA = StoreModel::factory()->create();
|
||||||
|
$storeB = StoreModel::factory()->create();
|
||||||
|
$storeC = StoreModel::factory()->create();
|
||||||
|
|
||||||
|
$this->makeRecord($storeA, '55.00', '2026-08-01 09:00:00');
|
||||||
|
$this->makeRecord($storeB, '-20.00', '2026-08-01 10:00:00');
|
||||||
|
// C 区间内无记录,但被选中 → 整列填 0
|
||||||
|
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
Excel::fake();
|
||||||
|
$this->get('/recon/container-return/export?start_date=2026-08-01&end_date=2026-08-02&store_ids=' . $storeA->id . ',' . $storeC->id)
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
Excel::assertDownloaded(
|
||||||
|
'回筐记录_2026-08-01_2026-08-02.xlsx',
|
||||||
|
static function (ContainerReturnExport $export) use ($storeA, $storeC): bool {
|
||||||
|
$rows = $export->collection()->values();
|
||||||
|
// 列头仅含所选门店(B 的记录被忽略)
|
||||||
|
if ($rows[3] !== ['日期', $storeA->name, $storeC->name, '合计']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 08-01:A 55、C 填 0;08-02 全 0
|
||||||
|
if ($rows[4] !== ['2026-08-01', 55.0, 0.0, 55.0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($rows[5] !== ['2026-08-02', 0.0, 0.0, 0.0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows[6] === ['合计', 55.0, 0.0, 55.0];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 参数校验:缺少日期 / 结束早于开始 / 区间超 366 天 → 拒绝 */
|
||||||
|
public function test_export_invalid_params_rejected(): void
|
||||||
|
{
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
|
||||||
|
$this->get('/recon/container-return/export')
|
||||||
|
->assertJsonPath('success', false);
|
||||||
|
$this->get('/recon/container-return/export?start_date=2026-08-10&end_date=2026-08-01')
|
||||||
|
->assertJsonPath('success', false);
|
||||||
|
$this->get('/recon/container-return/export?start_date=2026-01-01&end_date=2027-06-30')
|
||||||
|
->assertJsonPath('success', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 区间内无记录且未选门店 → 拒绝 */
|
||||||
|
public function test_export_empty_range_rejected(): void
|
||||||
|
{
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
|
||||||
|
$this->get('/recon/container-return/export?start_date=2026-08-01&end_date=2026-08-03')
|
||||||
|
->assertJsonPath('success', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 无 recon.containerReturn.export 权限点 → 拦截 */
|
||||||
|
public function test_export_requires_export_permission(): void
|
||||||
|
{
|
||||||
|
// 先建一个占位用户:每个测试方法内首个系统用户自增 id=1,
|
||||||
|
// SysAccessToken::can() 对 tokenable_id==1(超管)放行全部权限,会绕过 abilities 校验
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
// 仅持有查询权限的用户
|
||||||
|
$this->actingAsSysUser(['recon.containerReturn.query']);
|
||||||
|
|
||||||
|
$response = $this->get('/recon/container-return/export?start_date=2026-08-01&end_date=2026-08-03');
|
||||||
|
$this->assertFalse($response->json('success'), '缺少权限点应被拦截');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { downloadBlob } from '@/api/common/download.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 汇总导出回筐记录:行=日期(同日记录合并),列=门店,
|
||||||
|
* 单元格为抵扣(附加)金额,含行合计/列合计,当天门店无记录填 0
|
||||||
|
*
|
||||||
|
* @param storeIds 门店ID列表,空数组=全部门店
|
||||||
|
*/
|
||||||
|
export async function exportContainerReturns(startDate: string, endDate: string, storeIds: number[]) {
|
||||||
|
return downloadBlob(
|
||||||
|
'/recon/container-return/export',
|
||||||
|
{
|
||||||
|
start_date: startDate,
|
||||||
|
end_date: endDate,
|
||||||
|
...(storeIds.length > 0 ? { store_ids: storeIds.join(',') } : {}),
|
||||||
|
},
|
||||||
|
'回筐记录.xlsx'
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,23 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Typography } from 'antd';
|
import { Button, DatePicker, Form, Modal, Select, Typography } from 'antd';
|
||||||
|
import { DownloadOutlined } from '@ant-design/icons';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import XinTable from '@/components/XinTable';
|
import XinTable from '@/components/XinTable';
|
||||||
import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings.ts';
|
import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings.ts';
|
||||||
import type IContainerReturn from '@/domain/iContainerReturn.ts';
|
import type IContainerReturn from '@/domain/iContainerReturn.ts';
|
||||||
import { getStoreOptions } from '@/api/customer/store.ts';
|
import { getStoreOptions } from '@/api/customer/store.ts';
|
||||||
|
import { exportContainerReturns } from '@/api/recon/containerReturn.ts';
|
||||||
import type IStore from '@/domain/iStore.ts';
|
import type IStore from '@/domain/iStore.ts';
|
||||||
|
import AuthButton from '@/components/AuthButton';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
|
/** 汇总导出表单值 */
|
||||||
|
interface ExportFormValues {
|
||||||
|
date_range: [dayjs.Dayjs, dayjs.Dayjs];
|
||||||
|
store_ids?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
/** 有符号数量展示:正数=压筐(橙色 +N),负数=回筐(绿色 N),0 置灰 */
|
/** 有符号数量展示:正数=压筐(橙色 +N),负数=回筐(绿色 N),0 置灰 */
|
||||||
const SignedNum: React.FC<{ value?: number }> = ({ value }) => {
|
const SignedNum: React.FC<{ value?: number }> = ({ value }) => {
|
||||||
@@ -39,10 +50,31 @@ const SignedAmount: React.FC<{ value?: string }> = ({ value }) => {
|
|||||||
const ContainerReturnPage: React.FC = () => {
|
const ContainerReturnPage: React.FC = () => {
|
||||||
const [stores, setStores] = useState<IStore[]>([]);
|
const [stores, setStores] = useState<IStore[]>([]);
|
||||||
|
|
||||||
|
// 汇总导出(日期区间 + 门店:行=日期,列=门店,单元格为抵扣/附加金额)
|
||||||
|
const [exportOpen, setExportOpen] = useState(false);
|
||||||
|
const [exporting, setExporting] = useState(false);
|
||||||
|
const [exportForm] = Form.useForm<ExportFormValues>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getStoreOptions().then((res) => setStores(res.data.data ?? []));
|
getStoreOptions().then((res) => setStores(res.data.data ?? []));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/** 提交汇总导出:下载失败时 downloadBlob 内部已提示 */
|
||||||
|
const handleExport = async (values: ExportFormValues) => {
|
||||||
|
const [start, end] = values.date_range;
|
||||||
|
setExporting(true);
|
||||||
|
try {
|
||||||
|
await exportContainerReturns(
|
||||||
|
start.format('YYYY-MM-DD'),
|
||||||
|
end.format('YYYY-MM-DD'),
|
||||||
|
values.store_ids ?? []
|
||||||
|
);
|
||||||
|
setExportOpen(false);
|
||||||
|
} finally {
|
||||||
|
setExporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const columns: XinTableColumn<IContainerReturn>[] = [
|
const columns: XinTableColumn<IContainerReturn>[] = [
|
||||||
{
|
{
|
||||||
title: '门店',
|
title: '门店',
|
||||||
@@ -143,6 +175,17 @@ const ContainerReturnPage: React.FC = () => {
|
|||||||
editShow: false,
|
editShow: false,
|
||||||
deleteShow: false,
|
deleteShow: false,
|
||||||
formProps: false,
|
formProps: false,
|
||||||
|
toolBarRender: (dom) => [
|
||||||
|
<AuthButton key="export" auth="recon.containerReturn.export">
|
||||||
|
<Button icon={<DownloadOutlined />} onClick={() => setExportOpen(true)}>
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
|
</AuthButton>,
|
||||||
|
dom.columnSetting,
|
||||||
|
dom.hideBorder,
|
||||||
|
dom.reload,
|
||||||
|
dom.columnHeight,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -150,10 +193,55 @@ const ContainerReturnPage: React.FC = () => {
|
|||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<Title level={3}>回筐记录</Title>
|
<Title level={3}>回筐记录</Title>
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
周转筐/托盘跟账单走:生成账单时按填写数量自动写入;正数=压筐(附加金额),负数=回筐(抵扣金额),数量为 0 不生成记录。
|
周转筐/托盘跟账单走:生成账单时按填写数量自动写入;正数=压筐(附加金额),负数=回筐(抵扣金额),数量为 0 不生成记录;
|
||||||
|
可按日期区间与门店汇总导出(行=日期,列=门店,含行列合计)。
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<XinTable<IContainerReturn> {...tableProps} />
|
<XinTable<IContainerReturn> {...tableProps} />
|
||||||
|
|
||||||
|
{/* 汇总导出:日期区间 + 门店,同日记录合并,无记录填 0 */}
|
||||||
|
<Modal
|
||||||
|
title="导出回筐记录"
|
||||||
|
open={exportOpen}
|
||||||
|
onCancel={() => setExportOpen(false)}
|
||||||
|
onOk={() => exportForm.submit()}
|
||||||
|
confirmLoading={exporting}
|
||||||
|
okText="导出"
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
<div className="py-2 text-gray-500">
|
||||||
|
按日期区间与门店导出抵扣(附加)金额汇总表:行=日期(同日记录合并),列=门店,
|
||||||
|
当天门店无记录填 0,含行合计与列合计。门店不选默认导出全部门店。
|
||||||
|
</div>
|
||||||
|
<Form
|
||||||
|
form={exportForm}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleExport}
|
||||||
|
initialValues={{
|
||||||
|
date_range: [dayjs().startOf('month'), dayjs()],
|
||||||
|
store_ids: [],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="日期区间"
|
||||||
|
name="date_range"
|
||||||
|
rules={[{ required: true, message: '请选择日期区间' }]}
|
||||||
|
>
|
||||||
|
<RangePicker className="w-full" allowClear={false} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="门店" name="store_ids">
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
allowClear
|
||||||
|
maxTagCount="responsive"
|
||||||
|
placeholder="全部门店"
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
options={stores.map((s) => ({ label: s.name, value: s.id }))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user