initial commit
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Node modules folder
|
||||||
|
node_modules/
|
||||||
|
.vscode/
|
16
kelly-report.code-workspace
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "src/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"terminal.integrated.defaultProfile.windows": "PowerShell",
|
||||||
|
"chat.promptFilesLocations": {
|
||||||
|
"./promps.md": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
package.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "kelly-report",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"generator-office": "^3.0.1",
|
||||||
|
"yo": "^5.1.0"
|
||||||
|
}
|
||||||
|
}
|
5157
pnpm-lock.yaml
generated
Normal file
3
prompts.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
I want to create an office add-in for excel
|
||||||
|
|
||||||
|
this add-in will read current excel data, and analyse the data, then group infomation to a new excel file
|
8
src/.eslintrc.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"office-addins"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:office-addins/recommended"
|
||||||
|
]
|
||||||
|
}
|
71
src/README.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# Build Excel add-ins using Office Add-ins Development Kit
|
||||||
|
|
||||||
|
Excel add-ins are integrations built by third parties into Excel by using [Excel JavaScript API](https://learn.microsoft.com/en-us/office/dev/add-ins/reference/overview/excel-add-ins-reference-overview) and [Office Platform capabilities](https://learn.microsoft.com/en-us/office/dev/add-ins/overview/office-add-ins).
|
||||||
|
|
||||||
|
## How to run this project
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js (the latest LTS version). Visit the [Node.js site](https://nodejs.org/) to download and install the right version for your operating system. To verify that you've already installed these tools, run the commands `node -v` and `npm -v` in your terminal.
|
||||||
|
- Office connected to a Microsoft 365 subscription. You might qualify for a Microsoft 365 E5 developer subscription through the [Microsoft 365 Developer Program](https://developer.microsoft.com/microsoft-365/dev-program), see [FAQ](https://learn.microsoft.com/office/developer-program/microsoft-365-developer-program-faq#who-qualifies-for-a-microsoft-365-e5-developer-subscription-) for details. Alternatively, you can [sign up for a 1-month free trial](https://www.microsoft.com/microsoft-365/try?rtc=1) or [purchase a Microsoft 365 plan](https://www.microsoft.com/microsoft-365/buy/compare-all-microsoft-365-products).
|
||||||
|
|
||||||
|
### Run the add-in using Office Add-ins Development Kit extension
|
||||||
|
|
||||||
|
1. **Open the Office Add-ins Development Kit**
|
||||||
|
|
||||||
|
In the **Activity Bar**, select the **Office Add-ins Development Kit** icon to open the extension.
|
||||||
|
|
||||||
|
1. **Preview Your Office Add-in (F5)**
|
||||||
|
|
||||||
|
Select **Preview Your Office Add-in(F5)** to launch the add-in and debug the code. In the Quick Pick menu, select the option **Excel Desktop (Edge Chromium)**.
|
||||||
|
|
||||||
|
The extension then checks that the prerequisites are met before debugging starts. Check the terminal for detailed information if there are issues with your environment. After this process, the Excel desktop application launches and sideloads the add-in.
|
||||||
|
|
||||||
|
1. **Stop Previewing Your Office Add-in**
|
||||||
|
|
||||||
|
Once you are finished testing and debugging the add-in, select **Stop Previewing Your Office Add-in**. This closes the web server and removes the add-in from the registry and cache.
|
||||||
|
|
||||||
|
## Use the add-in project
|
||||||
|
|
||||||
|
The add-in project that you've created contains sample code for a basic task pane add-in.
|
||||||
|
|
||||||
|
## Explore the add-in code
|
||||||
|
|
||||||
|
To explore an Office add-in project, you can start with the key files listed below.
|
||||||
|
|
||||||
|
- The `./manifest.xml` file in the root directory of the project defines the settings and capabilities of the add-in. <br>You can check whether your manifest file is valid by selecting **Validate Manifest File** option from the Office Add-ins Development Kit.
|
||||||
|
- The `./src/taskpane/taskpane.html` file contains the HTML markup for the task pane.
|
||||||
|
- The `./src/taskpane/**/*.tsx` file contains the react code and Office JavaScript API code that facilitates interaction between the task pane and the Excel application.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If you have problems running the add-in, take these steps.
|
||||||
|
|
||||||
|
- Close any open instances of Excel.
|
||||||
|
- Close the previous web server started for the add-in with the **Stop Previewing Your Office Add-in** Office Add-ins Development Kit extension option.
|
||||||
|
|
||||||
|
If you still have problems, see [troubleshoot development errors](https://learn.microsoft.com//office/dev/add-ins/testing/troubleshoot-development-errors) or [create a GitHub issue](https://aka.ms/officedevkitnewissue) and we'll help you.
|
||||||
|
|
||||||
|
For information on running the add-in on Excel on the web, see [Sideload Office Add-ins to Office on the web](https://learn.microsoft.com/office/dev/add-ins/testing/sideload-office-add-ins-for-testing).
|
||||||
|
|
||||||
|
For information on debugging on older versions of Office, see [Debug add-ins using developer tools in Microsoft Edge Legacy](https://learn.microsoft.com/office/dev/add-ins/testing/debug-add-ins-using-devtools-edge-legacy).
|
||||||
|
|
||||||
|
## Make code changes
|
||||||
|
|
||||||
|
All the information about Office Add-ins is found in our [official documentation](https://learn.microsoft.com/office/dev/add-ins/overview/office-add-ins). You can also explore more samples in the Office Add-ins Development Kit. Select **View Samples** to see more samples of real-world scenarios.
|
||||||
|
|
||||||
|
If you edit the manifest as part of your changes, use the **Validate Manifest File** option in the Office Add-ins Development Kit. This shows you errors in the manifest syntax.
|
||||||
|
|
||||||
|
## Engage with the team
|
||||||
|
|
||||||
|
Did you experience any problems? [Create an issue](https://aka.ms/officedevkitnewissue) and we'll help you out.
|
||||||
|
|
||||||
|
Want to learn more about new features and best practices for the Office platform? [Join the Microsoft Office Add-ins community call](https://learn.microsoft.com/office/dev/add-ins/overview/office-add-ins-community-call).
|
||||||
|
|
||||||
|
## Copyright
|
||||||
|
|
||||||
|
Copyright (c) 2024 Microsoft Corporation. All rights reserved.
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
BIN
src/assets/icon-128.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/icon-16.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/icon-32.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/assets/icon-64.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
src/assets/icon-80.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
src/assets/icon_16.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/icon_32.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
src/assets/logo-filled.png
Normal file
After Width: | Height: | Size: 12 KiB |
13
src/babel.config.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
"targets": {
|
||||||
|
"esmodules": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@babel/preset-typescript"
|
||||||
|
]
|
||||||
|
}
|
85
src/manifest.xml
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="TaskPaneApp">
|
||||||
|
<Id>e518fe68-bcc8-4572-a8ff-8e927e9426cf</Id>
|
||||||
|
<Version>1.0.0.0</Version>
|
||||||
|
<ProviderName>Contoso</ProviderName>
|
||||||
|
<DefaultLocale>en-US</DefaultLocale>
|
||||||
|
<DisplayName DefaultValue="kelly-report-2"/>
|
||||||
|
<Description DefaultValue="A template to get started."/>
|
||||||
|
<IconUrl DefaultValue="https://localhost:3000/assets/icon-32.png"/>
|
||||||
|
<HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-64.png"/>
|
||||||
|
<!-- <SupportUrl DefaultValue="https://www.contoso.com/help"/> -->
|
||||||
|
<AppDomains>
|
||||||
|
<AppDomain>https://www.contoso.com</AppDomain>
|
||||||
|
</AppDomains>
|
||||||
|
<Hosts>
|
||||||
|
<Host Name="Workbook"/>
|
||||||
|
</Hosts>
|
||||||
|
<DefaultSettings>
|
||||||
|
<SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
|
||||||
|
</DefaultSettings>
|
||||||
|
<Permissions>ReadWriteDocument</Permissions>
|
||||||
|
<VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
|
||||||
|
<Hosts>
|
||||||
|
<Host xsi:type="Workbook">
|
||||||
|
<DesktopFormFactor>
|
||||||
|
<GetStarted>
|
||||||
|
<Title resid="GetStarted.Title"/>
|
||||||
|
<Description resid="GetStarted.Description"/>
|
||||||
|
<LearnMoreUrl resid="GetStarted.LearnMoreUrl"/>
|
||||||
|
</GetStarted>
|
||||||
|
<FunctionFile resid="Commands.Url"/>
|
||||||
|
<ExtensionPoint xsi:type="PrimaryCommandSurface">
|
||||||
|
<OfficeTab id="TabHome">
|
||||||
|
<Group id="CommandsGroup">
|
||||||
|
<Label resid="CommandsGroup.Label"/>
|
||||||
|
<Icon>
|
||||||
|
<bt:Image size="16" resid="Icon.16x16"/>
|
||||||
|
<bt:Image size="32" resid="Icon.32x32"/>
|
||||||
|
<bt:Image size="80" resid="Icon.80x80"/>
|
||||||
|
</Icon>
|
||||||
|
<Control xsi:type="Button" id="TaskpaneButton">
|
||||||
|
<Label resid="TaskpaneButton.Label"/>
|
||||||
|
<Supertip>
|
||||||
|
<Title resid="TaskpaneButton.Label"/>
|
||||||
|
<Description resid="TaskpaneButton.Tooltip"/>
|
||||||
|
</Supertip>
|
||||||
|
<Icon>
|
||||||
|
<bt:Image size="16" resid="Icon.16x16"/>
|
||||||
|
<bt:Image size="32" resid="Icon.32x32"/>
|
||||||
|
<bt:Image size="80" resid="Icon.80x80"/>
|
||||||
|
</Icon>
|
||||||
|
<Action xsi:type="ShowTaskpane">
|
||||||
|
<TaskpaneId>ButtonId1</TaskpaneId>
|
||||||
|
<SourceLocation resid="Taskpane.Url"/>
|
||||||
|
</Action>
|
||||||
|
</Control>
|
||||||
|
</Group>
|
||||||
|
</OfficeTab>
|
||||||
|
</ExtensionPoint>
|
||||||
|
</DesktopFormFactor>
|
||||||
|
</Host>
|
||||||
|
</Hosts>
|
||||||
|
<Resources>
|
||||||
|
<bt:Images>
|
||||||
|
<bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon_16.png"/>
|
||||||
|
<bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon_32.png"/>
|
||||||
|
<bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/>
|
||||||
|
</bt:Images>
|
||||||
|
<bt:Urls>
|
||||||
|
<bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://go.microsoft.com/fwlink/?LinkId=276812"/>
|
||||||
|
<bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/>
|
||||||
|
<bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/>
|
||||||
|
</bt:Urls>
|
||||||
|
<bt:ShortStrings>
|
||||||
|
<bt:String id="GetStarted.Title" DefaultValue="Get started with your sample add-in!"/>
|
||||||
|
<bt:String id="CommandsGroup.Label" DefaultValue="报表工具"/>
|
||||||
|
<bt:String id="TaskpaneButton.Label" DefaultValue="JQ 销售母表导入"/>
|
||||||
|
</bt:ShortStrings>
|
||||||
|
<bt:LongStrings>
|
||||||
|
<bt:String id="GetStarted.Description" DefaultValue="Your sample add-in loaded successfully. Go to the HOME tab and click the 'Show Task Pane' button to get started."/>
|
||||||
|
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="通过导入销售母表,生成年销售报表"/>
|
||||||
|
</bt:LongStrings>
|
||||||
|
</Resources>
|
||||||
|
</VersionOverrides>
|
||||||
|
</OfficeApp>
|
14035
src/package-lock.json
generated
Normal file
64
src/package.json
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"name": "office-addin-taskpane",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/OfficeDev/Office-Addin-TaskPane.git"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"config": {
|
||||||
|
"app_to_debug": "excel",
|
||||||
|
"app_type_to_debug": "desktop",
|
||||||
|
"dev_server_port": 3000
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --mode production",
|
||||||
|
"build:dev": "webpack --mode development",
|
||||||
|
"dev-server": "webpack serve --mode development",
|
||||||
|
"lint": "office-addin-lint check",
|
||||||
|
"lint:fix": "office-addin-lint fix",
|
||||||
|
"prettier": "office-addin-lint prettier",
|
||||||
|
"signin": "office-addin-dev-settings m365-account login",
|
||||||
|
"signout": "office-addin-dev-settings m365-account logout",
|
||||||
|
"start": "office-addin-debugging start manifest.xml",
|
||||||
|
"stop": "office-addin-debugging stop manifest.xml",
|
||||||
|
"validate": "office-addin-manifest validate manifest.xml",
|
||||||
|
"watch": "webpack --mode development --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"core-js": "^3.36.0",
|
||||||
|
"regenerator-runtime": "^0.14.1",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.24.0",
|
||||||
|
"@babel/preset-env": "^7.25.4",
|
||||||
|
"@babel/preset-typescript": "^7.23.3",
|
||||||
|
"@types/office-js": "^1.0.377",
|
||||||
|
"@types/office-runtime": "^1.0.35",
|
||||||
|
"babel-loader": "^9.1.3",
|
||||||
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
|
"eslint-plugin-office-addins": "^4.0.3",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"html-loader": "^5.0.0",
|
||||||
|
"html-webpack-plugin": "^5.6.0",
|
||||||
|
"office-addin-cli": "^2.0.3",
|
||||||
|
"office-addin-debugging": "^6.0.3",
|
||||||
|
"office-addin-dev-certs": "^2.0.3",
|
||||||
|
"office-addin-lint": "^3.0.3",
|
||||||
|
"office-addin-manifest": "^2.0.3",
|
||||||
|
"office-addin-prettier-config": "^2.0.1",
|
||||||
|
"os-browserify": "^0.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"source-map-loader": "^5.0.0",
|
||||||
|
"typescript": "^5.4.2",
|
||||||
|
"webpack": "^5.95.0",
|
||||||
|
"webpack-cli": "^5.1.4",
|
||||||
|
"webpack-dev-server": "5.1.0"
|
||||||
|
},
|
||||||
|
"prettier": "office-addin-prettier-config",
|
||||||
|
"browserslist": [
|
||||||
|
"last 2 versions",
|
||||||
|
"ie 11"
|
||||||
|
]
|
||||||
|
}
|
18
src/src/commands/commands.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!-- Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. -->
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||||
|
|
||||||
|
<!-- Office JavaScript API -->
|
||||||
|
<script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
35
src/src/commands/commands.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||||
|
* See LICENSE in the project root for license information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* global Office */
|
||||||
|
|
||||||
|
Office.onReady(() => {
|
||||||
|
// If needed, Office.js is ready to be called.
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a notification when the add-in command is executed.
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
function action(event: Office.AddinCommands.Event) {
|
||||||
|
const message: Office.NotificationMessageDetails = {
|
||||||
|
type: Office.MailboxEnums.ItemNotificationMessageType.InformationalMessage,
|
||||||
|
message: "Performed action.",
|
||||||
|
icon: "Icon.80x80",
|
||||||
|
persistent: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show a notification message.
|
||||||
|
Office.context.mailbox.item.notificationMessages.replaceAsync(
|
||||||
|
"ActionPerformanceNotification",
|
||||||
|
message
|
||||||
|
);
|
||||||
|
|
||||||
|
// Be sure to indicate when the add-in command function is complete.
|
||||||
|
event.completed();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the function with Office.
|
||||||
|
Office.actions.associate("action", action);
|
442
src/src/process.ts
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
import { Sheet } from "xlsx";
|
||||||
|
|
||||||
|
type SalesInvoiceData = {
|
||||||
|
"业务": string;
|
||||||
|
"发票日期": string;
|
||||||
|
"发货日期": number;
|
||||||
|
"合計金額": number;
|
||||||
|
"含税单价": number;
|
||||||
|
"备注": string;
|
||||||
|
"客户名称": string;
|
||||||
|
"数量(KG)": number;
|
||||||
|
"未税金额": number;
|
||||||
|
"申报所属月份": string;
|
||||||
|
"發票號碼": string;
|
||||||
|
"稅金13%": number;
|
||||||
|
"英文品名": string;
|
||||||
|
"订单号码": string;
|
||||||
|
}
|
||||||
|
type ClientInfo = {
|
||||||
|
area: string,
|
||||||
|
industry: string,
|
||||||
|
capacityPerYear: string,
|
||||||
|
name: string,
|
||||||
|
purchaseManager: string,
|
||||||
|
purchaseManagerPhone: string,
|
||||||
|
purchaser: string,
|
||||||
|
purchaserPhone: string,
|
||||||
|
address: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const readClientInfo = async (context: Excel.RequestContext) => {
|
||||||
|
// 取得「客戶資料」工作表
|
||||||
|
const sheet = context.workbook.worksheets.getItem("客戶資料");
|
||||||
|
// 取得已用範圍
|
||||||
|
const usedRange = sheet.getUsedRange();
|
||||||
|
usedRange.load(["values"]);
|
||||||
|
await context.sync();
|
||||||
|
// 取得所有資料
|
||||||
|
const rawClientInfo = usedRange.values;
|
||||||
|
const [header, ...clientInfoDataSource] = rawClientInfo as string[][];
|
||||||
|
const clientInfoMap: { [key: string]: ClientInfo } = {}
|
||||||
|
for (const clientInfo of clientInfoDataSource) {
|
||||||
|
clientInfoMap[clientInfo[3]] = {
|
||||||
|
area: clientInfo[0],
|
||||||
|
industry: clientInfo[1],
|
||||||
|
capacityPerYear: clientInfo[2],
|
||||||
|
name: clientInfo[3],
|
||||||
|
purchaseManager: clientInfo[4],
|
||||||
|
purchaseManagerPhone: clientInfo[5],
|
||||||
|
purchaser: clientInfo[6],
|
||||||
|
purchaserPhone: clientInfo[7],
|
||||||
|
address: clientInfo[8]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientInfoMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const groupByUsers = (invoiceDataList: SalesInvoiceData[]) => {
|
||||||
|
const groupedUsers: Record<string, SalesInvoiceData[]> = {};
|
||||||
|
|
||||||
|
invoiceDataList.forEach((invoiceData) => { // Iterate over each user in the input array
|
||||||
|
const key = invoiceData["业务"]?.toLowerCase(); // Use the "客户名称" field as the grouping key
|
||||||
|
|
||||||
|
if (!groupedUsers[key]) { // If this key doesn't exist in the groupedUsers object, create it
|
||||||
|
groupedUsers[key] = []; // Initialize it with an empty array
|
||||||
|
}
|
||||||
|
|
||||||
|
groupedUsers[key].push(invoiceData); // Push the current user into the appropriate group
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupedUsers; // Return the grouped data
|
||||||
|
}
|
||||||
|
const groupInvoiceDataByProduct = (userInvoiceList: SalesInvoiceData[]) => {
|
||||||
|
return userInvoiceList.reduce((accGroupedInvoiceData, invoiceData) => {
|
||||||
|
const clientName = invoiceData['客户名称']
|
||||||
|
if (!accGroupedInvoiceData[clientName]) {
|
||||||
|
accGroupedInvoiceData[clientName] = { total: {} }
|
||||||
|
}
|
||||||
|
const invoiceDataByClient = accGroupedInvoiceData[clientName];
|
||||||
|
|
||||||
|
const productName = invoiceData['英文品名'];
|
||||||
|
if (!invoiceDataByClient[productName]) {
|
||||||
|
invoiceDataByClient[productName] = {}
|
||||||
|
}
|
||||||
|
const invoiceDataByProduct = invoiceDataByClient[productName];
|
||||||
|
|
||||||
|
const monthName = invoiceData['申报所属月份'];
|
||||||
|
if (!invoiceDataByProduct[monthName]) {
|
||||||
|
invoiceDataByProduct[monthName] = {
|
||||||
|
quantity: 0,
|
||||||
|
price: 0,
|
||||||
|
totalPrice: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const invoiceDataByMonth = invoiceDataByProduct[monthName];
|
||||||
|
invoiceDataByMonth.quantity += isFinite(invoiceData['数量(KG)']) ? invoiceData['数量(KG)'] : 0;
|
||||||
|
invoiceDataByMonth.price += invoiceData['含税单价'];
|
||||||
|
invoiceDataByMonth.totalPrice += invoiceData['合計金額'];
|
||||||
|
|
||||||
|
if (!invoiceDataByClient.total[monthName]) {
|
||||||
|
invoiceDataByClient.total[monthName] = {
|
||||||
|
quantity: 0,
|
||||||
|
price: 0,
|
||||||
|
totalPrice: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
invoiceDataByClient.total[monthName].quantity += invoiceDataByMonth.quantity;
|
||||||
|
invoiceDataByClient.total[monthName].price += invoiceDataByMonth.price;
|
||||||
|
invoiceDataByClient.total[monthName].totalPrice += invoiceDataByMonth.totalPrice;
|
||||||
|
|
||||||
|
return accGroupedInvoiceData;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const genSalesMonthData = ({
|
||||||
|
userSheet,
|
||||||
|
rowNumber,
|
||||||
|
user,
|
||||||
|
clientInfo,
|
||||||
|
clientName,
|
||||||
|
invoiceData,
|
||||||
|
productName
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const salesNameCell = userSheet.getRange(`A${rowNumber}`);
|
||||||
|
salesNameCell.values = [[user]];
|
||||||
|
|
||||||
|
const clientNameCell = userSheet.getRange(`E${rowNumber}`);
|
||||||
|
const targetClientInfo = clientInfo[clientName];
|
||||||
|
if (targetClientInfo) {
|
||||||
|
userSheet.getRange(`B${rowNumber}:J${rowNumber}`).values = [[
|
||||||
|
targetClientInfo.area,
|
||||||
|
targetClientInfo.industry,
|
||||||
|
targetClientInfo.capacityPerYear,
|
||||||
|
targetClientInfo.name,
|
||||||
|
targetClientInfo.purchaseManager,
|
||||||
|
targetClientInfo.purchaseManagerPhone,
|
||||||
|
targetClientInfo.purchaser,
|
||||||
|
targetClientInfo.purchaserPhone,
|
||||||
|
targetClientInfo.address
|
||||||
|
]]
|
||||||
|
} else {
|
||||||
|
clientNameCell.values = [[clientName]];
|
||||||
|
}
|
||||||
|
|
||||||
|
userSheet.getRange(`K${rowNumber}`).values = [[productName]];
|
||||||
|
const monthColStartIndex = 12;
|
||||||
|
const monthColEndIndex = 47;
|
||||||
|
const productYearTotal = { price: 0, quantity: 0, totalPrice: 0 }
|
||||||
|
for (let i = monthColStartIndex; i <= monthColEndIndex; i += 3) {
|
||||||
|
const monthIndex = Math.floor((i - monthColStartIndex) / 3) + 1; // 1~12月
|
||||||
|
const monthName = `${monthIndex}月`;
|
||||||
|
//@ts-ignore
|
||||||
|
const monthData = invoiceData[monthName];
|
||||||
|
if (!monthData) continue;
|
||||||
|
|
||||||
|
userSheet.getRangeByIndexes(rowNumber - 1, i, 1, 1).values = [[monthData.price]];
|
||||||
|
userSheet.getRangeByIndexes(rowNumber - 1, i + 1, 1, 1).values = [[monthData.quantity]];
|
||||||
|
userSheet.getRangeByIndexes(rowNumber - 1, i + 2, 1, 1).values = [[monthData.totalPrice]];
|
||||||
|
|
||||||
|
productYearTotal.price += monthData.price;
|
||||||
|
productYearTotal.quantity += monthData.quantity;
|
||||||
|
productYearTotal.totalPrice += monthData.totalPrice;
|
||||||
|
}
|
||||||
|
userSheet.getRangeByIndexes(rowNumber - 1, 48, 1, 1).values = [[productYearTotal.quantity]];
|
||||||
|
userSheet.getRangeByIndexes(rowNumber - 1, 49, 1, 1).values = [[productYearTotal.totalPrice]];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateTotalSheet = async (context: Excel.RequestContext, invoiceDataByUser: Record<string, SalesInvoiceData[]>) => {
|
||||||
|
const clientInfo = await readClientInfo(context);
|
||||||
|
|
||||||
|
for (const user of Object.keys(invoiceDataByUser)) {
|
||||||
|
let userSheet;
|
||||||
|
try {
|
||||||
|
userSheet = context.workbook.worksheets.add(`JQ Total(${user})`); // Create a new worksheet for each user
|
||||||
|
await context.sync();
|
||||||
|
} catch (e) {
|
||||||
|
userSheet = context.workbook.worksheets.getItem(`JQ Total(${user})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const usedRange = userSheet.getUsedRangeOrNullObject();
|
||||||
|
usedRange.load("isNullObject");
|
||||||
|
await context.sync();
|
||||||
|
if (!usedRange.isNullObject) {
|
||||||
|
usedRange.clear();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 若無 UsedRange 也忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.trackedObjects.add(userSheet); // Track the worksheet object
|
||||||
|
|
||||||
|
await preparedTotalHeader(userSheet, context); // Sync to ensure the used range is loaded
|
||||||
|
// insert data into the worksheet
|
||||||
|
const userInvoiceList = invoiceDataByUser[user]; // Get the data for the current user
|
||||||
|
let rowNumber = 4; // Start from the 4th row (index 3) to avoid overwriting headers
|
||||||
|
const groupedInvoiceList = groupInvoiceDataByProduct(userInvoiceList);
|
||||||
|
for (const [clientName, productInvoiceData] of Object.entries(groupedInvoiceList)) {
|
||||||
|
for (const [productName, montlyInvoiceData] of Object.entries(productInvoiceData)) {
|
||||||
|
if (!productName || productName === 'total') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
genSalesMonthData({ userSheet, rowNumber, user, clientName, clientInfo, productName, invoiceData: montlyInvoiceData });
|
||||||
|
rowNumber++; // Move to the next row for the next invoice data
|
||||||
|
}
|
||||||
|
|
||||||
|
genSalesMonthData({ userSheet, rowNumber, user, clientName, clientInfo, productName: '總銷售額', invoiceData: productInvoiceData.total });
|
||||||
|
rowNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.trackedObjects.remove(userSheet); // Remove the worksheet object from tracking
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalSheet;
|
||||||
|
try {
|
||||||
|
totalSheet = context.workbook.worksheets.add(`JQ Total(Total)`); // Create a new worksheet for each user
|
||||||
|
await context.sync();
|
||||||
|
} catch (e) {
|
||||||
|
totalSheet = context.workbook.worksheets.getItem(`JQ Total(Total)`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const usedRange = totalSheet.getUsedRangeOrNullObject();
|
||||||
|
usedRange.load("isNullObject");
|
||||||
|
await context.sync();
|
||||||
|
if (!usedRange.isNullObject) {
|
||||||
|
usedRange.clear();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 若無 UsedRange 也忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.sync(); // Sync the changes with Excel
|
||||||
|
await mergeUserSheetsToTotalSheet(context);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 合併所有 JQ Total(user) sheet 到 JQ Total(Total)
|
||||||
|
async function mergeUserSheetsToTotalSheet(context: Excel.RequestContext) {
|
||||||
|
// 取得所有 worksheet 名稱
|
||||||
|
const sheets = context.workbook.worksheets;
|
||||||
|
sheets.load("items/name");
|
||||||
|
await context.sync();
|
||||||
|
// 篩選出 JQ Total( 開頭且不是 JQ Total(Total) 的 sheet
|
||||||
|
const userSheetNames = sheets.items
|
||||||
|
.map(s => s.name)
|
||||||
|
.filter(name => name.startsWith("JQ Total(") && name !== "JQ Total(Total)");
|
||||||
|
// 取得總表
|
||||||
|
let totalSheet;
|
||||||
|
try {
|
||||||
|
totalSheet = context.workbook.worksheets.add("JQ Total(Total)");
|
||||||
|
await context.sync();
|
||||||
|
} catch {
|
||||||
|
totalSheet = context.workbook.worksheets.getItem("JQ Total(Total)");
|
||||||
|
try {
|
||||||
|
const usedRange = totalSheet.getUsedRangeOrNullObject();
|
||||||
|
usedRange.load("isNullObject");
|
||||||
|
await context.sync();
|
||||||
|
if (!usedRange.isNullObject) {
|
||||||
|
usedRange.clear();
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
await preparedTotalHeader(totalSheet, context);
|
||||||
|
let allRows = [];
|
||||||
|
for (const sheetName of userSheetNames) {
|
||||||
|
const sheet = context.workbook.worksheets.getItem(sheetName);
|
||||||
|
const usedRange = sheet.getUsedRange();
|
||||||
|
usedRange.load(["values", "rowCount", "columnCount"]);
|
||||||
|
await context.sync();
|
||||||
|
// 跳過前3行header
|
||||||
|
const dataRows = usedRange.values.slice(3);
|
||||||
|
if (dataRows.length > 0) {
|
||||||
|
allRows = allRows.concat(dataRows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allRows.length > 0) {
|
||||||
|
totalSheet.getRangeByIndexes(3, 0, allRows.length, allRows[0].length).values = allRows;
|
||||||
|
}
|
||||||
|
const totalRowIndex = allRows.length + 4;
|
||||||
|
totalSheet.getRange(`A${totalRowIndex}:E${totalRowIndex + 3}`).merge();
|
||||||
|
totalSheet.getRange(`A${totalRowIndex}`).values = [['總結']];
|
||||||
|
|
||||||
|
totalSheet.getRange(`K${totalRowIndex}`).values = [['總銷量及金額(未稅)']];
|
||||||
|
totalSheet.getRange(`K${totalRowIndex + 1}`).values = [['總銷量及金額(含稅)']];
|
||||||
|
totalSheet.getRange(`K${totalRowIndex + 2}`).values = [['總銷量及金額(外銷)']];
|
||||||
|
totalSheet.getRange(`K${totalRowIndex + 3}`).values = [['總銷量及金額(內外銷)']];
|
||||||
|
|
||||||
|
totalSheet.getRange(`N${totalRowIndex}`).values = [[`=(SUM(N4:N${totalRowIndex - 1})/2)+N354`]];
|
||||||
|
totalSheet.getRange(`Q${totalRowIndex}`).values = [[`=(SUM(Q4:Q${totalRowIndex - 1})/2)+Q354`]];
|
||||||
|
totalSheet.getRange(`T${totalRowIndex}`).values = [[`=(SUM(T4:T${totalRowIndex - 1})/2)+T354`]];
|
||||||
|
totalSheet.getRange(`W${totalRowIndex}`).values = [[`=(SUM(W4:W${totalRowIndex - 1})/2)+W354`]];
|
||||||
|
totalSheet.getRange(`Z${totalRowIndex}`).values = [[`=(SUM(Z4:Z${totalRowIndex - 1})/2)+Z354`]];
|
||||||
|
totalSheet.getRange(`AC${totalRowIndex}`).values = [[`=(SUM(AC4:AC${totalRowIndex - 1})/2)+AC354`]];
|
||||||
|
totalSheet.getRange(`AF${totalRowIndex}`).values = [[`=(SUM(AF4:AF${totalRowIndex - 1})/2)+AF354`]];
|
||||||
|
totalSheet.getRange(`AI${totalRowIndex}`).values = [[`=(SUM(AI4:AI${totalRowIndex - 1})/2)+AI354`]];
|
||||||
|
totalSheet.getRange(`AL${totalRowIndex}`).values = [[`=(SUM(AL4:AL${totalRowIndex - 1})/2)+AL354`]];
|
||||||
|
totalSheet.getRange(`AO${totalRowIndex}`).values = [[`=(SUM(AO4:AO${totalRowIndex - 1})/2)+AO354`]];
|
||||||
|
totalSheet.getRange(`AR${totalRowIndex}`).values = [[`=(SUM(AR4:AR${totalRowIndex - 1})/2)+AR354`]];
|
||||||
|
totalSheet.getRange(`AU${totalRowIndex}`).values = [[`=(SUM(AU4:AU${totalRowIndex - 1})/2)+AU354`]];
|
||||||
|
totalSheet.getRange(`AW${totalRowIndex}`).values = [[`=(SUM(AW4:AW${totalRowIndex - 1})/2)+AW354`]];
|
||||||
|
|
||||||
|
totalSheet.getRange(`O${totalRowIndex}`).values = [[`=((SUM(O4:O${totalRowIndex - 1})/2) - O${totalRowIndex + 2})/1.13`]];
|
||||||
|
totalSheet.getRange(`R${totalRowIndex}`).values = [[`=((SUM(R4:R${totalRowIndex - 1})/2) - R${totalRowIndex + 2})/1.13`]];
|
||||||
|
totalSheet.getRange(`U${totalRowIndex}`).values = [[`=((SUM(U4:U${totalRowIndex - 1})/2) - U${totalRowIndex + 2})/1.13`]];
|
||||||
|
totalSheet.getRange(`X${totalRowIndex}`).values = [[`=((SUM(X4:X${totalRowIndex - 1})/2) - X${totalRowIndex + 2})/1.13`]];
|
||||||
|
totalSheet.getRange(`AA${totalRowIndex}`).values = [[`=((SUM(AA4:AA${totalRowIndex - 1})/2) - AA${totalRowIndex + 2})/1.13`]];
|
||||||
|
totalSheet.getRange(`AD${totalRowIndex}`).values = [[`=((SUM(AD4:AD${totalRowIndex - 1})/2) - AD${totalRowIndex + 2})/1.13`]];
|
||||||
|
totalSheet.getRange(`AG${totalRowIndex}`).values = [[`=((SUM(AG4:AG${totalRowIndex - 1})/2) - AG${totalRowIndex + 2})/1.13`]];
|
||||||
|
totalSheet.getRange(`AJ${totalRowIndex}`).values = [[`=((SUM(AJ4:AJ${totalRowIndex - 1})/2) - AJ${totalRowIndex + 2})/1.13`]];
|
||||||
|
totalSheet.getRange(`AM${totalRowIndex}`).values = [[`=((SUM(AM4:AM${totalRowIndex - 1})/2) - AM${totalRowIndex + 2})/1.13`]];
|
||||||
|
totalSheet.getRange(`AP${totalRowIndex}`).values = [[`=((SUM(AP4:AP${totalRowIndex - 1})/2) - AP${totalRowIndex + 2})/1.13`]];
|
||||||
|
totalSheet.getRange(`AS${totalRowIndex}`).values = [[`=((SUM(AS4:AS${totalRowIndex - 1})/2) - AS${totalRowIndex + 2})/1.13`]];
|
||||||
|
totalSheet.getRange(`AV${totalRowIndex}`).values = [[`=((SUM(AV4:AV${totalRowIndex - 1})/2) - AV${totalRowIndex + 2})/1.13`]];
|
||||||
|
totalSheet.getRange(`AX${totalRowIndex}`).values = [[`=((SUM(AX4:AX${totalRowIndex - 1})/2) - AX${totalRowIndex + 2})/1.13`]];
|
||||||
|
|
||||||
|
totalSheet.getRange(`N${totalRowIndex + 1}`).values = [[`=(SUM(N4:N${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`Q${totalRowIndex + 1}`).values = [[`=(SUM(Q4:Q${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`T${totalRowIndex + 1}`).values = [[`=(SUM(T4:T${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`W${totalRowIndex + 1}`).values = [[`=(SUM(W4:W${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`Z${totalRowIndex + 1}`).values = [[`=(SUM(Z4:Z${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AC${totalRowIndex + 1}`).values = [[`=(SUM(AC4:AC${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AF${totalRowIndex + 1}`).values = [[`=(SUM(AF4:AF${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AI${totalRowIndex + 1}`).values = [[`=(SUM(AI4:AI${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AL${totalRowIndex + 1}`).values = [[`=(SUM(AL4:AL${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AO${totalRowIndex + 1}`).values = [[`=(SUM(AO4:AO${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AR${totalRowIndex + 1}`).values = [[`=(SUM(AR4:AR${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AU${totalRowIndex + 1}`).values = [[`=(SUM(AU4:AU${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AW${totalRowIndex + 1}`).values = [[`=(SUM(AW4:AW${totalRowIndex - 1})/2)`]];
|
||||||
|
|
||||||
|
totalSheet.getRange(`O${totalRowIndex + 1}`).values = [[`=(SUM(O4:O${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`R${totalRowIndex + 1}`).values = [[`=(SUM(R4:R${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`U${totalRowIndex + 1}`).values = [[`=(SUM(U4:U${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`X${totalRowIndex + 1}`).values = [[`=(SUM(X4:X${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AA${totalRowIndex + 1}`).values = [[`=(SUM(AA4:AA${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AD${totalRowIndex + 1}`).values = [[`=(SUM(AD4:AD${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AG${totalRowIndex + 1}`).values = [[`=(SUM(AG4:AG${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AJ${totalRowIndex + 1}`).values = [[`=(SUM(AJ4:AJ${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AM${totalRowIndex + 1}`).values = [[`=(SUM(AM4:AM${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AP${totalRowIndex + 1}`).values = [[`=(SUM(AP4:AP${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AS${totalRowIndex + 1}`).values = [[`=(SUM(AS4:AS${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AV${totalRowIndex + 1}`).values = [[`=(SUM(AV4:AV${totalRowIndex - 1})/2)`]];
|
||||||
|
totalSheet.getRange(`AX${totalRowIndex + 1}`).values = [[`=(SUM(AX4:AX${totalRowIndex - 1})/2)`]];
|
||||||
|
|
||||||
|
//TODO: totalRowIndex + 2
|
||||||
|
|
||||||
|
totalSheet.getRange(`N${totalRowIndex + 3}`).values = [[`=N${totalRowIndex}+N${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`Q${totalRowIndex + 3}`).values = [[`=Q${totalRowIndex}+Q${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`T${totalRowIndex + 3}`).values = [[`=T${totalRowIndex}+T${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`W${totalRowIndex + 3}`).values = [[`=W${totalRowIndex}+W${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`Z${totalRowIndex + 3}`).values = [[`=Z${totalRowIndex}+Z${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AC${totalRowIndex + 3}`).values = [[`=AC${totalRowIndex}+AC${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AF${totalRowIndex + 3}`).values = [[`=AF${totalRowIndex}+AF${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AI${totalRowIndex + 3}`).values = [[`=AI${totalRowIndex}+AI${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AL${totalRowIndex + 3}`).values = [[`=AL${totalRowIndex}+AL${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AO${totalRowIndex + 3}`).values = [[`=AO${totalRowIndex}+AO${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AR${totalRowIndex + 3}`).values = [[`=AR${totalRowIndex}+AR${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AU${totalRowIndex + 3}`).values = [[`=AU${totalRowIndex}+AU${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AW${totalRowIndex + 3}`).values = [[`=AW${totalRowIndex}+AW${totalRowIndex + 2}`]];
|
||||||
|
|
||||||
|
totalSheet.getRange(`O${totalRowIndex + 3}`).values = [[`=O${totalRowIndex}+O${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`R${totalRowIndex + 3}`).values = [[`=R${totalRowIndex}+R${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`U${totalRowIndex + 3}`).values = [[`=U${totalRowIndex}+U${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`X${totalRowIndex + 3}`).values = [[`=X${totalRowIndex}+X${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AA${totalRowIndex + 3}`).values = [[`=AA${totalRowIndex}+AA${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AD${totalRowIndex + 3}`).values = [[`=AD${totalRowIndex}+AD${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AG${totalRowIndex + 3}`).values = [[`=AG${totalRowIndex}+AG${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AJ${totalRowIndex + 3}`).values = [[`=AJ${totalRowIndex}+AJ${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AM${totalRowIndex + 3}`).values = [[`=AM${totalRowIndex}+AM${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AP${totalRowIndex + 3}`).values = [[`=AP${totalRowIndex}+AP${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AS${totalRowIndex + 3}`).values = [[`=AS${totalRowIndex}+AS${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AV${totalRowIndex + 3}`).values = [[`=AV${totalRowIndex}+AV${totalRowIndex + 2}`]];
|
||||||
|
totalSheet.getRange(`AX${totalRowIndex + 3}`).values = [[`=AX${totalRowIndex}+AX${totalRowIndex + 2}`]];
|
||||||
|
|
||||||
|
await context.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preparedTotalHeader(userSheet: Excel.Worksheet, context: Excel.RequestContext) {
|
||||||
|
userSheet.getRange("A1:L1").merge();
|
||||||
|
userSheet.getRange("A1").values = [['客戶資料']];
|
||||||
|
|
||||||
|
userSheet.getRange("A2:A3").merge();
|
||||||
|
userSheet.getRange("A2").values = [['業務']];
|
||||||
|
userSheet.getRange("B2:B3").merge();
|
||||||
|
userSheet.getRange("B2").values = [['地區']];
|
||||||
|
userSheet.getRange("C2:C3").merge();
|
||||||
|
userSheet.getRange("C2").values = [['行業']];
|
||||||
|
userSheet.getRange("D2:D3").merge();
|
||||||
|
userSheet.getRange("D2").values = [['產能/年']];
|
||||||
|
userSheet.getRange("E2:E3").merge();
|
||||||
|
userSheet.getRange("E2").values = [['名稱']];
|
||||||
|
userSheet.getRange("F2:F3").merge();
|
||||||
|
userSheet.getRange("F2").values = [['採購主管']];
|
||||||
|
userSheet.getRange("G2:G3").merge();
|
||||||
|
userSheet.getRange("G2").values = [['電話']];
|
||||||
|
userSheet.getRange("H2:H3").merge();
|
||||||
|
userSheet.getRange("H2").values = [['採購']];
|
||||||
|
userSheet.getRange("I2:I3").merge();
|
||||||
|
userSheet.getRange("I2").values = [['電話']];
|
||||||
|
userSheet.getRange("J2:J3").merge();
|
||||||
|
userSheet.getRange("J2").values = [['地址']];
|
||||||
|
userSheet.getRange("K2:K3").merge();
|
||||||
|
userSheet.getRange("K2").values = [['產品']];
|
||||||
|
userSheet.getRange("L2:L3").merge();
|
||||||
|
userSheet.getRange("L2").values = [['需求/月']];
|
||||||
|
|
||||||
|
await context.sync(); // Sync to ensure the worksheet is fully initialized
|
||||||
|
|
||||||
|
let usedRange = userSheet.getUsedRange(); // Get the used range of the worksheet
|
||||||
|
usedRange.load(); // Load the used range
|
||||||
|
await context.sync(); // Sync to ensure the used range is loaded
|
||||||
|
|
||||||
|
const lastColumn = usedRange.getLastColumn(); // Get the last column of the used range
|
||||||
|
lastColumn.load("columnIndex"); // Load the columnIndex property of the last column
|
||||||
|
await context.sync(); // Sync to ensure the columnIndex is loaded
|
||||||
|
|
||||||
|
const startColumn = lastColumn.columnIndex + 1; // Start from the next column
|
||||||
|
userSheet.getRangeByIndexes(0, startColumn, 1, 38).merge();
|
||||||
|
userSheet.getRangeByIndexes(0, startColumn, 1, 1).values = [['年度销售表']];
|
||||||
|
|
||||||
|
// Set month headers
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const startColumnIndex = startColumn + i * 3; // Each month takes 3 columns
|
||||||
|
userSheet.getRangeByIndexes(1, startColumnIndex, 1, 3).merge();
|
||||||
|
userSheet.getRangeByIndexes(1, startColumnIndex, 1, 1).values = [[`${i + 1}月`]];
|
||||||
|
userSheet.getRangeByIndexes(2, startColumnIndex, 1, 1).values = [['單價']];
|
||||||
|
userSheet.getRangeByIndexes(2, startColumnIndex + 1, 1, 1).values = [['數量']];
|
||||||
|
userSheet.getRangeByIndexes(2, startColumnIndex + 2, 1, 1).values = [['金額']];
|
||||||
|
}
|
||||||
|
userSheet.getRangeByIndexes(1, startColumn + 36, 1, 2).merge();
|
||||||
|
userSheet.getRangeByIndexes(1, startColumn + 36, 1, 1).values = [[`Total`]];
|
||||||
|
userSheet.getRangeByIndexes(2, startColumn + 36, 1, 1).values = [[`總數量`]];
|
||||||
|
userSheet.getRangeByIndexes(2, startColumn + 37, 1, 1).values = [[`總金額`]];
|
||||||
|
// here I want to set all cell text to center
|
||||||
|
usedRange = userSheet.getUsedRange(); // Get the used range of the worksheet
|
||||||
|
usedRange.load(); // Load the used range
|
||||||
|
await context.sync(); // Sync to ensure the used range is loaded
|
||||||
|
usedRange.format.horizontalAlignment = "Center"; // Center align the header row
|
||||||
|
await context.sync();
|
||||||
|
}
|
106
src/src/processForcast.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
type SalesForcastData = {
|
||||||
|
year: string
|
||||||
|
salesName: string,
|
||||||
|
forcast: number[],
|
||||||
|
turnover?: number[]
|
||||||
|
completion?: number;
|
||||||
|
accCompletion?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const analiseSalesForcast = async (context: Excel.RequestContext, rawData: Array<Array<string | number>>) => {
|
||||||
|
const [yearRow, headerRow, ...dataRows] = rawData;
|
||||||
|
const salesForcatDataList: SalesForcastData[] = [];
|
||||||
|
headerRow.splice(0, 1);
|
||||||
|
const monthData = {
|
||||||
|
year: yearRow[1] as string,
|
||||||
|
forcast: headerRow
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const salesRow of dataRows) {
|
||||||
|
const salesForcastData = {
|
||||||
|
year: yearRow[1] as string,
|
||||||
|
salesName: salesRow[0] as string,
|
||||||
|
forcast: []
|
||||||
|
}
|
||||||
|
for (let i = 1; i < salesRow.length; i++) {
|
||||||
|
salesForcastData.forcast.push(salesRow[i]);
|
||||||
|
}
|
||||||
|
salesForcatDataList.push(salesForcastData)
|
||||||
|
}
|
||||||
|
// 新增或取得「業務月達成率」sheet
|
||||||
|
let sheet;
|
||||||
|
let isNewSheet = true;
|
||||||
|
try {
|
||||||
|
sheet = context.workbook.worksheets.add("業務月達成率");
|
||||||
|
await context.sync(); // Sync the changes with Excel
|
||||||
|
} catch {
|
||||||
|
sheet = context.workbook.worksheets.getItem("業務月達成率");
|
||||||
|
try {
|
||||||
|
const usedRange = sheet.getUsedRangeOrNullObject();
|
||||||
|
usedRange.load("isNullObject");
|
||||||
|
await context.sync();
|
||||||
|
if (!usedRange.isNullObject) {
|
||||||
|
usedRange.clear();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 若無 UsedRange 也忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 寫入轉置後的資料
|
||||||
|
// 產生月份陣列
|
||||||
|
const monthCount = monthData?.forcast.length || 0;
|
||||||
|
// 合併 A1:A2 並填入年份
|
||||||
|
try {
|
||||||
|
const yearValue = monthData?.year || yearRow[1];
|
||||||
|
// A1:A2 合併並填年份
|
||||||
|
sheet.getRange("A1:A2").merge();
|
||||||
|
sheet.getRange("A1").values = [[yearValue.toString()]];
|
||||||
|
sheet.getRange("A1").numberFormat = [['@']];
|
||||||
|
// A3~: 填入月份
|
||||||
|
if (monthCount > 0) {
|
||||||
|
sheet.getRange(`A3:A${monthCount + 2}`).values = monthData.forcast.map(m => [m]);
|
||||||
|
}
|
||||||
|
// B1~: 填入 salesName 並合併
|
||||||
|
let col = 1; // B欄開始
|
||||||
|
for (let i = 0; i < salesForcatDataList.length; i++) {
|
||||||
|
const salesForcastData = salesForcatDataList[i];
|
||||||
|
let rowIndex = 0;
|
||||||
|
const startCol = col;
|
||||||
|
const endCol = col + 4;
|
||||||
|
sheet.getRangeByIndexes(rowIndex, startCol, 1, 2).merge();
|
||||||
|
sheet.getRangeByIndexes(rowIndex, startCol, 1, 1).values = [[salesForcastData.salesName]];
|
||||||
|
sheet.getRangeByIndexes(rowIndex, startCol + 2, 2, 1).merge();
|
||||||
|
sheet.getRangeByIndexes(rowIndex, startCol + 2, 1, 1).values = [['完成%']];
|
||||||
|
sheet.getRangeByIndexes(rowIndex, startCol + 3, 2, 1).merge();
|
||||||
|
sheet.getRangeByIndexes(rowIndex, startCol + 3, 1, 1).values = [['累積達成率']];
|
||||||
|
|
||||||
|
rowIndex++;
|
||||||
|
sheet.getRangeByIndexes(rowIndex, startCol, 1, 1).values = [['Y-forecast']];
|
||||||
|
sheet.getRangeByIndexes(rowIndex, startCol + 1, 1, 1).values = [['turnover']];
|
||||||
|
|
||||||
|
rowIndex++;
|
||||||
|
for (const forcastMonth of salesForcastData.forcast) {
|
||||||
|
const cell = sheet.getRangeByIndexes(rowIndex, startCol, 1, 1);
|
||||||
|
cell.values = [[forcastMonth]];
|
||||||
|
cell.numberFormat = [["#,##0"]]; // 千分號格式
|
||||||
|
rowIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
col = endCol;
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.sync(); // Sync the changes with Excel
|
||||||
|
// 自適應所有已用欄位寬度
|
||||||
|
const usedRange = sheet.getUsedRangeOrNullObject();
|
||||||
|
usedRange.load("isNullObject");
|
||||||
|
await context.sync();
|
||||||
|
if (!usedRange.isNullObject) {
|
||||||
|
usedRange.format.autofitColumns();
|
||||||
|
}
|
||||||
|
await context.sync();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('catch err', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
81
src/src/taskpane/taskpane.css
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||||
|
* See LICENSE in the project root for license information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-welcome__header {
|
||||||
|
padding: 20px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
padding-top: 100px;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-welcome__main {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
-webkit-flex-wrap: nowrap;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-flex: 1 0 0;
|
||||||
|
flex: 1 0 0;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-welcome__main > h2 {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-welcome__features {
|
||||||
|
list-style-type: none;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-welcome__features.ms-List .ms-ListItem {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-welcome__features.ms-List .ms-ListItem > .ms-Icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-welcome__action.ms-Button--hero {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-Button.ms-Button--hero .ms-Button-label {
|
||||||
|
color: #4da1e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-Button.ms-Button--hero:hover .ms-Button-label,
|
||||||
|
.ms-Button.ms-Button--hero:focus .ms-Button-label{
|
||||||
|
color: #94c9f1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
52
src/src/taskpane/taskpane.html
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<!-- Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. -->
|
||||||
|
<!-- This file shows how to design a first-run page that provides a welcome screen to the user about the features of the add-in. -->
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Contoso Task Pane Add-in</title>
|
||||||
|
|
||||||
|
<!-- Office JavaScript API -->
|
||||||
|
<script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||||
|
|
||||||
|
<!-- For more information on Fluent UI, visit https://developer.microsoft.com/fluentui#/. -->
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-core/11.1.0/css/fabric.min.css" />
|
||||||
|
|
||||||
|
<!-- Template styles -->
|
||||||
|
<link href="taskpane.css" rel="stylesheet" type="text/css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="ms-font-m ms-welcome ms-Fabric">
|
||||||
|
<main id="app-body" class="ms-welcome__main" style="color: #fff">
|
||||||
|
<h3> JQ 銷售表統計 </h3>
|
||||||
|
<div>
|
||||||
|
<b> Step 1:</b> 請在"銷售年度預估" 和 "產品年度預估" 工作簿中填寫年度預估值,然後點擊下方按鈕開始生成《業務月達成 》和《年度JQ產品》表
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div role="button" id="forcast" class="ms-welcome__action ms-Button ms-Button--hero ms-font-xl">
|
||||||
|
<span class="ms-Button-label">制定年度預估</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<hr style="margin: 24px 0; border: 0; border-top: 2px solid #cccccc;" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b> Step 2:</b> 請導入銷售數據表, 並點擊下方 "導入銷售表" 按鈕。
|
||||||
|
<br>程序將會根據銷售表的數據生成各個業務的JQ Total表
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<input id="sales-invoce-data-source" type="file" placeholder="" name="sales-invoce-data-source" accept=".xlsx, .xls" />
|
||||||
|
</div>
|
||||||
|
<div role="button" id="run" class="ms-welcome__action ms-Button ms-Button--hero ms-font-xl">
|
||||||
|
<span class="ms-Button-label">導入銷售表</span>
|
||||||
|
</div>
|
||||||
|
<p><label id="item-subject"></label></p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
135
src/src/taskpane/taskpane.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||||
|
* See LICENSE in the project root for license information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* global console, document, Excel, Office */
|
||||||
|
import * as XLSX from "xlsx";
|
||||||
|
import { generateTotalSheet, groupByUsers } from "../process";
|
||||||
|
import { analiseSalesForcast } from '../processForcast'
|
||||||
|
|
||||||
|
Office.onReady((info) => {
|
||||||
|
if (info.host === Office.HostType.Excel) {
|
||||||
|
document.getElementById("app-body").style.display = "flex";
|
||||||
|
document.getElementById("run").onclick = run;
|
||||||
|
document.getElementById("forcast").onclick = genForcast;
|
||||||
|
|
||||||
|
|
||||||
|
// Add event listener for file input
|
||||||
|
const fileInput = document.getElementById("sales-invoce-data-source") as HTMLInputElement;
|
||||||
|
fileInput.addEventListener("change", handleFileUpload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let salesInvoiceData: any[] = [];
|
||||||
|
|
||||||
|
function handleFileUpload(event: Event) {
|
||||||
|
const fileInput = event.target as HTMLInputElement;
|
||||||
|
if (fileInput.files && fileInput.files[0]) {
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const data = new Uint8Array(e.target?.result as ArrayBuffer);
|
||||||
|
const workbook = XLSX.read(data, { type: "array" });
|
||||||
|
|
||||||
|
// Get the first sheet name
|
||||||
|
const sheetName = workbook.SheetNames[0];
|
||||||
|
|
||||||
|
// Get the worksheet
|
||||||
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
|
|
||||||
|
// Convert the worksheet to JSON
|
||||||
|
const dataSource = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
||||||
|
|
||||||
|
// Convert the rows to objects using the headers
|
||||||
|
const [_, headers, ...rows] = dataSource;
|
||||||
|
salesInvoiceData = tableAsObjects(headers, rows);
|
||||||
|
|
||||||
|
// Log the data or display it
|
||||||
|
// console.log("Excel Data:", salesInvoiceData);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableAsObjects = (headers, rows) => rows.map((row) => {
|
||||||
|
const obj: Record<string, any> = {};
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
obj[header.replace(/\r\n/g, '')] = row[index];
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
|
||||||
|
let isRuningTotal = false;
|
||||||
|
export async function run(e) {
|
||||||
|
if (isRuningForcast) return;
|
||||||
|
|
||||||
|
isRuningForcast = true;
|
||||||
|
const buttonLabel: HTMLElement = e.target;
|
||||||
|
buttonLabel.textContent = '正在生成...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Excel.run(async (context) => {
|
||||||
|
// Get the active worksheet
|
||||||
|
const sheet = context.workbook.worksheets.getActiveWorksheet();
|
||||||
|
if (!salesInvoiceData) {
|
||||||
|
console.error("No sales invice data imported!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const salesData = groupByUsers(salesInvoiceData);
|
||||||
|
await generateTotalSheet(context, salesData);
|
||||||
|
// Log the objects to the console
|
||||||
|
console.log("Table as objects:", salesData);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
isRuningForcast = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonLabel.textContent = '導入銷售表';
|
||||||
|
}
|
||||||
|
|
||||||
|
let isRuningForcast = false;
|
||||||
|
export async function genForcast(e) {
|
||||||
|
if (isRuningForcast) return;
|
||||||
|
|
||||||
|
isRuningForcast = true;
|
||||||
|
console.log('earo say the e.target', e.target);
|
||||||
|
const buttonLabel: HTMLElement = e.target;
|
||||||
|
buttonLabel.textContent = '正在生成...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Excel.run(async (context) => {
|
||||||
|
// 取得「銷售年度預估」工作表
|
||||||
|
const salesForecastSheet = context.workbook.worksheets.getItem("銷售年度預估");
|
||||||
|
const salesRange = salesForecastSheet.getUsedRange();
|
||||||
|
salesRange.load(["values"]);
|
||||||
|
|
||||||
|
// 取得「產品年度預估」工作表
|
||||||
|
const productForecastSheet = context.workbook.worksheets.getItem("產品年度預估");
|
||||||
|
const productRange = productForecastSheet.getUsedRange();
|
||||||
|
productRange.load(["values"]);
|
||||||
|
|
||||||
|
await context.sync();
|
||||||
|
|
||||||
|
// 轉為陣列
|
||||||
|
const salesForecastData = salesRange.values;
|
||||||
|
const productForecastData = productRange.values;
|
||||||
|
|
||||||
|
// 輸出到 console 以供檢查
|
||||||
|
console.log("銷售年度預估:", salesForecastData);
|
||||||
|
console.log("產品年度預估:", productForecastData);
|
||||||
|
|
||||||
|
await analiseSalesForcast(context, salesForecastData);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
isRuningForcast = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonLabel.textContent = '制定年度預估';
|
||||||
|
}
|
26
src/tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"noEmitOnError": true,
|
||||||
|
"outDir": "lib",
|
||||||
|
"sourceMap": true,
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"es2015",
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"lib",
|
||||||
|
"lib-amd"
|
||||||
|
],
|
||||||
|
"ts-node": {
|
||||||
|
"files": true
|
||||||
|
}
|
||||||
|
}
|
97
src/webpack.config.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
|
||||||
|
const devCerts = require("office-addin-dev-certs");
|
||||||
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
|
|
||||||
|
const urlDev = "https://localhost:3000/";
|
||||||
|
const urlProd = "https://www.contoso.com/"; // CHANGE THIS TO YOUR PRODUCTION DEPLOYMENT LOCATION
|
||||||
|
|
||||||
|
async function getHttpsOptions() {
|
||||||
|
const httpsOptions = await devCerts.getHttpsServerOptions();
|
||||||
|
return { ca: httpsOptions.ca, key: httpsOptions.key, cert: httpsOptions.cert };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (env, options) => {
|
||||||
|
const dev = options.mode === "development";
|
||||||
|
const config = {
|
||||||
|
devtool: "source-map",
|
||||||
|
entry: {
|
||||||
|
polyfill: ["core-js/stable", "regenerator-runtime/runtime"],
|
||||||
|
taskpane: ["./src/taskpane/taskpane.ts", "./src/taskpane/taskpane.html"],
|
||||||
|
commands: "./src/commands/commands.ts",
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
clean: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: [".ts", ".html", ".js"],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: "babel-loader"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.html$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: "html-loader",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jpg|jpeg|gif|ico)$/,
|
||||||
|
type: "asset/resource",
|
||||||
|
generator: {
|
||||||
|
filename: "assets/[name][ext][query]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: "taskpane.html",
|
||||||
|
template: "./src/taskpane/taskpane.html",
|
||||||
|
chunks: ["polyfill", "taskpane"],
|
||||||
|
}),
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
from: "assets/*",
|
||||||
|
to: "assets/[name][ext][query]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: "manifest*.xml",
|
||||||
|
to: "[name]" + "[ext]",
|
||||||
|
transform(content) {
|
||||||
|
if (dev) {
|
||||||
|
return content;
|
||||||
|
} else {
|
||||||
|
return content.toString().replace(new RegExp(urlDev, "g"), urlProd);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: "commands.html",
|
||||||
|
template: "./src/commands/commands.html",
|
||||||
|
chunks: ["polyfill", "commands"],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
devServer: {
|
||||||
|
headers: {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
type: "https",
|
||||||
|
options: env.WEBPACK_BUILD || options.https !== undefined ? options.https : await getHttpsOptions(),
|
||||||
|
},
|
||||||
|
port: process.env.npm_package_config_dev_server_port || 3000,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|