Skip to content

Commit 6432a2f

Browse files
committed
add ISMS & ESG xlsm download
1 parent 6855da8 commit 6432a2f

11 files changed

Lines changed: 606 additions & 0 deletions

pm/1150511_ESG標案.xlsm

5.94 MB
Binary file not shown.

pm/1150511_ISMS標案.xlsm

5.7 MB
Binary file not shown.

src/Tender.Desktop/App.xaml.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ private static void ConfigureServices(IServiceCollection services)
9090
services.AddSingleton<IMissedRunDetector, MissedRunDetector>();
9191
services.AddSingleton<IExcelExporter, ClosedXmlExcelExporter>();
9292
services.AddSingleton<IXlsmExporter, TemplateXlsmExporter>();
93+
services.AddSingleton<IIsmsXlsmExporter, IsmsTemplateXlsmExporter>();
94+
services.AddSingleton<IEsgXlsmExporter, EsgTemplateXlsmExporter>();
9395
services.AddSingleton<ISaveFileDialogService, WpfSaveFileDialogService>();
9496
services.AddSingleton<IErrorSummaryDialog, WpfErrorSummaryDialog>();
9597
services.AddSingleton<IUpdateChecker, GitHubUpdateChecker>();

src/Tender.Desktop/Services/IXlsmExporter.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,15 @@ Task ExportAsync(
1717
string savePath,
1818
CancellationToken ct = default);
1919
}
20+
21+
/// <summary>
22+
/// 以「ISMS 標案」範本為基底匯出(兩個工作表「全部資料」、「篩選」),保留 ISMS 專屬 VBA 巨集。
23+
/// 僅在「資安/無障礙」群組單獨勾選 ISMS、且「ESG/碳管理」群組無勾選時才允許執行。
24+
/// </summary>
25+
public interface IIsmsXlsmExporter : IXlsmExporter { }
26+
27+
/// <summary>
28+
/// 以「ESG 標案」範本為基底匯出(兩個工作表「全部資料」、「篩選」),保留 ESG 專屬 VBA 巨集。
29+
/// 僅在「ESG/碳管理」群組至少勾一項、且「資安/無障礙」群組無勾選時才允許執行。
30+
/// </summary>
31+
public interface IEsgXlsmExporter : IXlsmExporter { }

src/Tender.Desktop/Services/ProfiledTemplateXlsmExporter.cs

Lines changed: 332 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using Tender.Core.Models;
2+
3+
namespace Tender.Desktop.Services;
4+
5+
/// <summary>
6+
/// 每一份 .xlsm 範本(ISMS / ESG 等)有自己的 cellXfs 樣式表與 worksheet rel id,
7+
/// 此類保存「重寫 sheet1 / sheet2 時需要用到」的全部變數,避免每個匯出器重複寫一遍。
8+
///
9+
/// 樣式索引必須對應目標 .xlsm 的 styles.xml 中既有 cellXfs,否則開檔會出現「已修復的記錄」或樣式錯亂。
10+
/// </summary>
11+
internal sealed class XlsmTemplateProfile
12+
{
13+
public required string TemplateRelativePath { get; init; }
14+
15+
// ---- sheet1「全部資料」----
16+
public required string Sheet1SheetFormatPr { get; init; }
17+
public required string Sheet1Cols { get; init; }
18+
public required string Sheet1DrawingRelId { get; init; }
19+
public required string Sheet1PrinterRelId { get; init; }
20+
public required int Sheet1HeaderStyle { get; init; }
21+
public required int Sheet1HeaderJStyle { get; init; }
22+
public required int Sheet1HeaderHStyle { get; init; }
23+
public required int Sheet1DataBStyle { get; init; }
24+
public required int Sheet1DataCStyle { get; init; }
25+
public required int Sheet1DataDStyle { get; init; }
26+
public required int Sheet1DataEStyle { get; init; }
27+
public required int Sheet1DataFStyle { get; init; }
28+
public required int Sheet1DataGStyle { get; init; }
29+
public required int Sheet1DataHStyle { get; init; }
30+
public required int Sheet1DataIStyle { get; init; }
31+
public required int Sheet1DataJStyle { get; init; }
32+
33+
// ---- sheet2「篩選」----
34+
public required string Sheet2DrawingRelId { get; init; }
35+
public required string Sheet2PrinterRelId { get; init; }
36+
public required int Sheet2ColumnStyle { get; init; }
37+
public required int Sheet2DataBStyle { get; init; }
38+
39+
// ---- 1150511_ISMS標案.xlsm ----
40+
// styles.xml: cellXfs count=18;
41+
// sheet1.xml.rels: 印表 rId1637 / 繪圖 rId1638;
42+
// sheet2.xml.rels: 印表 rId1 / 繪圖 rId2。
43+
public static XlsmTemplateProfile Isms { get; } = new()
44+
{
45+
TemplateRelativePath = "Templates/tender-template-isms.xlsm",
46+
Sheet1SheetFormatPr = "<sheetFormatPr defaultRowHeight=\"16.5\"/>",
47+
Sheet1Cols =
48+
"<cols>" +
49+
"<col min=\"1\" max=\"1\" width=\"2.625\" style=\"6\" customWidth=\"1\"/>" +
50+
"<col min=\"2\" max=\"2\" width=\"45.5\" style=\"1\" customWidth=\"1\"/>" +
51+
"<col min=\"3\" max=\"3\" width=\"9\" style=\"6\"/>" +
52+
"<col min=\"4\" max=\"4\" width=\"10.5\" style=\"1\" customWidth=\"1\"/>" +
53+
"<col min=\"5\" max=\"5\" width=\"9.625\" style=\"1\" customWidth=\"1\"/>" +
54+
"<col min=\"6\" max=\"7\" width=\"10\" style=\"1\" customWidth=\"1\"/>" +
55+
"<col min=\"8\" max=\"8\" width=\"9.875\" style=\"1\" customWidth=\"1\"/>" +
56+
"<col min=\"9\" max=\"9\" width=\"10.125\" style=\"1\" customWidth=\"1\"/>" +
57+
"<col min=\"10\" max=\"16384\" width=\"9\" style=\"1\"/>" +
58+
"</cols>",
59+
Sheet1DrawingRelId = "rId1638",
60+
Sheet1PrinterRelId = "rId1637",
61+
Sheet1HeaderStyle = 9,
62+
Sheet1HeaderJStyle = 10,
63+
Sheet1HeaderHStyle = 9,
64+
Sheet1DataBStyle = 17,
65+
Sheet1DataCStyle = 12,
66+
Sheet1DataDStyle = 13,
67+
Sheet1DataEStyle = 14,
68+
Sheet1DataFStyle = 14,
69+
Sheet1DataGStyle = 14,
70+
Sheet1DataHStyle = 15,
71+
Sheet1DataIStyle = 13,
72+
Sheet1DataJStyle = 16,
73+
Sheet2DrawingRelId = "rId2",
74+
Sheet2PrinterRelId = "rId1",
75+
Sheet2ColumnStyle = 8,
76+
Sheet2DataBStyle = 1,
77+
};
78+
79+
// ---- 1150511_ESG標案.xlsm ----
80+
// styles.xml: cellXfs count=25;
81+
// sheet1.xml.rels: 印表 rId1637 / 繪圖 rId1638(與 ISMS 相同);
82+
// sheet2.xml.rels: 印表 rId3 / 繪圖 rId4。
83+
public static XlsmTemplateProfile Esg { get; } = new()
84+
{
85+
TemplateRelativePath = "Templates/tender-template-esg.xlsm",
86+
Sheet1SheetFormatPr = "<sheetFormatPr defaultRowHeight=\"16.5\"/>",
87+
Sheet1Cols =
88+
"<cols>" +
89+
"<col min=\"1\" max=\"1\" width=\"2.625\" style=\"10\" customWidth=\"1\"/>" +
90+
"<col min=\"2\" max=\"2\" width=\"45.5\" style=\"1\" customWidth=\"1\"/>" +
91+
"<col min=\"3\" max=\"3\" width=\"9\" style=\"10\"/>" +
92+
"<col min=\"4\" max=\"4\" width=\"10.5\" style=\"1\" customWidth=\"1\"/>" +
93+
"<col min=\"5\" max=\"5\" width=\"9.625\" style=\"1\" customWidth=\"1\"/>" +
94+
"<col min=\"6\" max=\"7\" width=\"10\" style=\"1\" customWidth=\"1\"/>" +
95+
"<col min=\"8\" max=\"8\" width=\"9.875\" style=\"1\" customWidth=\"1\"/>" +
96+
"<col min=\"9\" max=\"9\" width=\"10.125\" style=\"1\" customWidth=\"1\"/>" +
97+
"<col min=\"10\" max=\"16384\" width=\"9\" style=\"1\"/>" +
98+
"</cols>",
99+
Sheet1DrawingRelId = "rId1638",
100+
Sheet1PrinterRelId = "rId1637",
101+
Sheet1HeaderStyle = 14,
102+
Sheet1HeaderJStyle = 15,
103+
Sheet1HeaderHStyle = 14,
104+
Sheet1DataBStyle = 24,
105+
Sheet1DataCStyle = 6,
106+
Sheet1DataDStyle = 7,
107+
Sheet1DataEStyle = 9,
108+
Sheet1DataFStyle = 9,
109+
Sheet1DataGStyle = 9,
110+
Sheet1DataHStyle = 8,
111+
Sheet1DataIStyle = 7,
112+
Sheet1DataJStyle = 16,
113+
Sheet2DrawingRelId = "rId4",
114+
Sheet2PrinterRelId = "rId3",
115+
Sheet2ColumnStyle = 12,
116+
Sheet2DataBStyle = 24,
117+
};
118+
}
119+
120+
internal sealed class IsmsTemplateXlsmExporter : IIsmsXlsmExporter
121+
{
122+
private readonly ProfiledTemplateXlsmExporter _inner = new(XlsmTemplateProfile.Isms);
123+
124+
public Task ExportAsync(
125+
IReadOnlyList<TenderItem> allItems,
126+
IReadOnlyList<TenderItem> filteredItems,
127+
string savePath,
128+
CancellationToken ct = default)
129+
=> _inner.ExportAsync(allItems, filteredItems, savePath, ct);
130+
}
131+
132+
internal sealed class EsgTemplateXlsmExporter : IEsgXlsmExporter
133+
{
134+
private readonly ProfiledTemplateXlsmExporter _inner = new(XlsmTemplateProfile.Esg);
135+
136+
public Task ExportAsync(
137+
IReadOnlyList<TenderItem> allItems,
138+
IReadOnlyList<TenderItem> filteredItems,
139+
string savePath,
140+
CancellationToken ct = default)
141+
=> _inner.ExportAsync(allItems, filteredItems, savePath, ct);
142+
}
5.94 MB
Binary file not shown.
5.7 MB
Binary file not shown.

src/Tender.Desktop/Tender.Desktop.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@
3737
<Content Include="Templates\tender-template.xlsm">
3838
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3939
</Content>
40+
<Content Include="Templates\tender-template-isms.xlsm">
41+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
42+
</Content>
43+
<Content Include="Templates\tender-template-esg.xlsm">
44+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
45+
</Content>
4046
</ItemGroup>
4147

4248
</Project>

src/Tender.Desktop/ViewModels/DailyQueryViewModel.cs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public partial class DailyQueryViewModel : ObservableObject
1818
private readonly IClock _clock;
1919
private readonly IExcelExporter _excelExporter;
2020
private readonly IXlsmExporter _xlsmExporter;
21+
private readonly IIsmsXlsmExporter _ismsXlsmExporter;
22+
private readonly IEsgXlsmExporter _esgXlsmExporter;
2123
private readonly ISaveFileDialogService _saveDialog;
2224
private readonly IUserMarksRepository _userMarksRepo;
2325
private readonly ISavedSearchesRepository _savedSearchesRepo;
@@ -123,6 +125,8 @@ public DailyQueryViewModel(
123125
IClock clock,
124126
IExcelExporter excelExporter,
125127
IXlsmExporter xlsmExporter,
128+
IIsmsXlsmExporter ismsXlsmExporter,
129+
IEsgXlsmExporter esgXlsmExporter,
126130
ISaveFileDialogService saveDialog,
127131
IUserMarksRepository userMarksRepo,
128132
ISavedSearchesRepository savedSearchesRepo)
@@ -134,6 +138,8 @@ public DailyQueryViewModel(
134138
_clock = clock;
135139
_excelExporter = excelExporter;
136140
_xlsmExporter = xlsmExporter;
141+
_ismsXlsmExporter = ismsXlsmExporter;
142+
_esgXlsmExporter = esgXlsmExporter;
137143
_saveDialog = saveDialog;
138144
_userMarksRepo = userMarksRepo;
139145
_savedSearchesRepo = savedSearchesRepo;
@@ -539,6 +545,86 @@ private async Task ExportXlsmAsync(CancellationToken ct)
539545
}
540546
}
541547

548+
/// <summary>
549+
/// 是否可匯出 ISMS xlsm:「資安/無障礙」群組只勾選 ISMS、且「ESG/碳管理」群組未勾選任何項目。
550+
/// 由 ApplyFilter / OnAnyButtonToggled 觸發 OnPropertyChanged,連動 RelayCommand 的 CanExecuteChanged。
551+
/// </summary>
552+
public bool CanExportIsmsXlsm =>
553+
IsSoleSelection(SecurityGroupName, "ISMS") && IsGroupEmpty(EsgGroupName);
554+
555+
/// <summary>
556+
/// 是否可匯出 ESG xlsm:「ESG/碳管理」群組至少勾一項、且「資安/無障礙」群組未勾選任何項目。
557+
/// </summary>
558+
public bool CanExportEsgXlsm =>
559+
HasAnySelected(EsgGroupName) && IsGroupEmpty(SecurityGroupName);
560+
561+
private const string SecurityGroupName = "資安/無障礙";
562+
private const string EsgGroupName = "ESG/碳管理";
563+
564+
private bool IsGroupEmpty(string groupName)
565+
{
566+
var group = KeywordGroups.FirstOrDefault(g => g.Name == groupName);
567+
return group is null || !group.Buttons.Any(b => b.IsActive);
568+
}
569+
570+
private bool HasAnySelected(string groupName)
571+
{
572+
var group = KeywordGroups.FirstOrDefault(g => g.Name == groupName);
573+
return group is not null && group.Buttons.Any(b => b.IsActive);
574+
}
575+
576+
private bool IsSoleSelection(string groupName, string keyword)
577+
{
578+
var group = KeywordGroups.FirstOrDefault(g => g.Name == groupName);
579+
if (group is null) return false;
580+
var actives = group.Buttons.Where(b => b.IsActive).Select(b => b.Keyword).ToList();
581+
return actives.Count == 1 && actives[0] == keyword;
582+
}
583+
584+
[RelayCommand(CanExecute = nameof(CanExportIsmsXlsm))]
585+
private async Task ExportIsmsXlsmAsync(CancellationToken ct)
586+
{
587+
await ExportTemplateXlsmAsync(_ismsXlsmExporter, "ISMS", ct);
588+
}
589+
590+
[RelayCommand(CanExecute = nameof(CanExportEsgXlsm))]
591+
private async Task ExportEsgXlsmAsync(CancellationToken ct)
592+
{
593+
await ExportTemplateXlsmAsync(_esgXlsmExporter, "ESG", ct);
594+
}
595+
596+
private async Task ExportTemplateXlsmAsync(IXlsmExporter exporter, string tag, CancellationToken ct)
597+
{
598+
if (AllItems.Count == 0)
599+
{
600+
ErrorMessage = "目前沒有可匯出的資料";
601+
return;
602+
}
603+
604+
var suggested = IsRangeMode
605+
? $"{tag}標案_{Date:yyyyMMdd}~{DateTo!.Value:yyyyMMdd}.xlsm"
606+
: $"{tag}標案_{Date:yyyyMMdd}.xlsm";
607+
var savePath = _saveDialog.ShowSaveAsXlsm(suggested);
608+
if (string.IsNullOrEmpty(savePath)) return;
609+
610+
try
611+
{
612+
IsLoading = true;
613+
var allRaw = AllItems.Select(vm => vm.Item).ToList().AsReadOnly();
614+
var filteredRaw = FilteredItems.Select(vm => vm.Item).ToList().AsReadOnly();
615+
await exporter.ExportAsync(allRaw, filteredRaw, savePath, ct);
616+
ErrorMessage = $"已匯出 {tag}(全部 {AllItems.Count} / 篩選 {FilteredItems.Count} 筆,含巨集)→ {savePath}";
617+
}
618+
catch (Exception ex)
619+
{
620+
ErrorMessage = $"匯出失敗:{ex.Message}";
621+
}
622+
finally
623+
{
624+
IsLoading = false;
625+
}
626+
}
627+
542628
partial void OnKeywordQueryChanged(string? value) => ApplyFilter();
543629
partial void OnKeywordTitleOnlyChanged(bool value) => ApplyFilter();
544630
partial void OnKeywordExcludeChanged(bool value) => ApplyFilter();
@@ -640,5 +726,11 @@ private void ApplyFilter()
640726
.AsReadOnly();
641727

642728
FilteredItems = filtered;
729+
730+
// 關鍵字群組勾選改變會走 ApplyFilter,順便通知兩個匯出按鈕的 CanExecute
731+
OnPropertyChanged(nameof(CanExportIsmsXlsm));
732+
OnPropertyChanged(nameof(CanExportEsgXlsm));
733+
ExportIsmsXlsmCommand.NotifyCanExecuteChanged();
734+
ExportEsgXlsmCommand.NotifyCanExecuteChanged();
643735
}
644736
}

0 commit comments

Comments
 (0)