42 Commits

Author SHA1 Message Date
6f5a2e65bd Merge branch 'main' of https://git.17888.com.tw/yiming/17168ERP
# Conflicts:
#	web/admin/guadan/create.aspx
2025-11-12 12:53:25 +08:00
22d283dbe6 增加JSON IGNORE設定 2025-11-12 12:50:13 +08:00
3b5d366863 修改URL錯誤 2025-11-07 10:26:49 +08:00
c0404b8e70 添加sql執行文件,執行該文件就可以正常使用掛單和神祖牌位模組 2025-11-06 09:25:45 +08:00
1a6731e4c6 Merge remote-tracking branch 'origin/hkj1003' 2025-11-05 13:18:03 +08:00
b776b411b4 上传资料库结构 2025-10-29 13:54:27 +08:00
e9f17a5037 神祖牌位管理模組,掛單模組前端URL添加HTTP_HOST 2025-10-29 13:48:20 +08:00
05ef2e28f3 UPDATE 2025-10-20 11:54:40 +08:00
90ef949ca4 Merge remote-tracking branch 'origin/hkj1003' 2025-10-19 22:02:28 +08:00
39c9dd29e1 Merge branch 'yiming1013' 2025-10-19 22:01:45 +08:00
a6aa35176c Merge branch 'main' of https://git.17888.com.tw/yiming/17168ERP 2025-10-19 22:00:45 +08:00
6a43883d08 查詢範例 2025-10-19 21:59:22 +08:00
7d36d6b0a6 掛單調整 2025-10-14 13:44:23 +08:00
87a2c35300 Merge branch 'main' of https://git.17888.com.tw/yiming/17168ERP into yiming1013 2025-10-13 00:06:38 +08:00
7d57e292fe update doc 2025-10-13 00:04:59 +08:00
6fc82510cc 在掛單資料頁面,增加判斷掛單是否超時 2025-10-09 16:49:32 +08:00
4a36ce9c1c 挂单资料增加挂单单号查询,增加显示挂单单号 2025-10-03 16:41:34 +08:00
63cab37c87 Merge pull request 'hkj0918' (#2) from hkj0918 into main
Reviewed-on: #2
2025-10-03 08:12:49 +00:00
cd1e5c2cd0 挂单综合状态调整 2025-10-03 16:04:49 +08:00
d7b0f29296 统计修改 2025-10-03 14:37:33 +08:00
79854a2618 Merge pull request 'hkj0918' (#1) from hkj0918 into main
Reviewed-on: #1
2025-09-30 08:38:54 +00:00
93aaffd3d8 处理新建区域的时候出现的问题 2025-09-30 16:37:05 +08:00
71490b1fac guadan 2025-09-25 15:18:34 +08:00
c6bd763485 Merge remote-tracking branch 'origin/guadan0905' 2025-09-17 23:23:01 +08:00
ad06b77fae 添加生成舒文异常处理 2025-09-17 17:40:44 +08:00
af5b1f71fb 添加生成舒文异常处理 2025-09-17 17:38:29 +08:00
104f95eaec 修改舒文,牌位预览列印 2025-09-17 17:31:34 +08:00
c38dc55dff 处理挂单测试问题 2025-09-16 17:53:38 +08:00
40da17b414 调整挂单统计,修改删除区域报错 2025-09-16 11:49:02 +08:00
f1e3f555e6 修改资料库结构 2025-09-15 16:23:38 +08:00
72db7ca928 挂单模块URL 2025-09-15 14:24:31 +08:00
ebad44be71 挂单查询 2025-09-15 14:16:12 +08:00
68f2733530 Update data/SQL/17168_db_schema.sql 2025-09-15 02:34:28 +00:00
d752a01cc7 更新SCHEMA SQL,
疏文下載改相對路徑
2025-09-15 01:22:45 +08:00
03e366c34a Merge branch 'guadan0905' 2025-09-15 00:49:38 +08:00
d4748e415a update 2025-09-13 10:30:40 +08:00
42e46b4d35 Merge branch 'yiming-250910' 2025-09-12 15:53:48 +08:00
69b9b1bbd1 资料库数据文件上传 2025-09-12 15:45:11 +08:00
4419dfff64 把资料库产色的几个文件提交,汇出挂单功能的数据,状态数据要有才能运行 2025-09-12 13:48:04 +08:00
636b22849c Merge remote-tracking branch 'origin/guadan0905' 2025-09-11 00:48:50 +08:00
0b0ddc82bd 上传挂单资料库 2025-09-09 16:28:31 +08:00
ded24a8446 修改挂单功能 2025-09-09 16:25:28 +08:00
98 changed files with 15197 additions and 1049 deletions

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@
packages
obj/
*.user
*.log
*.log
**website.publishproj

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,176 @@
USE [17168erp_t2]
GO
/****** Object: Table [dbo].[AncestralTabletArea] Script Date: 2025/10/29 下午 01:32:57 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[AncestralTabletArea](
[AreaId] [int] IDENTITY(1,1) NOT NULL,
[AreaName] [nvarchar](10) NOT NULL,
[AreaCode] [nvarchar](20) NOT NULL,
[ParentAreaId] [int] NULL,
[AreaType] [nvarchar](10) NULL,
[Price] [int] NULL,
[SortOrder] [int] NULL,
[IsDisabled] [bit] NOT NULL,
[Description] [nvarchar](200) NULL,
PRIMARY KEY CLUSTERED
(
[AreaId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object: Table [dbo].[AncestralTabletPosition] Script Date: 2025/10/29 下午 01:32:57 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[AncestralTabletPosition](
[PositionId] [int] IDENTITY(1,1) NOT NULL,
[AreaId] [int] NOT NULL,
[PositionCode] [nvarchar](20) NOT NULL,
[PositionName] [nvarchar](50) NULL,
[Price] [int] NULL,
[StatusCode] [nvarchar](20) NULL,
[Description] [nvarchar](200) NULL,
[RowNo] [int] NULL,
[ColumnNo] [int] NULL,
PRIMARY KEY CLUSTERED
(
[PositionId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY],
CONSTRAINT [UQ_Position_Area_Code] UNIQUE NONCLUSTERED
(
[AreaId] ASC,
[PositionCode] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object: Table [dbo].[AncestralTabletPositionRecord] Script Date: 2025/10/29 下午 01:32:57 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[AncestralTabletPositionRecord](
[RecordId] [int] IDENTITY(1,1) NOT NULL,
[RegistrantCode] [nvarchar](20) NOT NULL,
[NPTitle] [nvarchar](30) NULL,
[NPStandDate] [date] NOT NULL,
[NPYangShang] [nvarchar](20) NULL,
[WPContent] [nvarchar](1000) NULL,
[CreatedAt] [datetime] NOT NULL,
[UpdatedAt] [datetime] NULL,
PRIMARY KEY CLUSTERED
(
[RecordId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object: Table [dbo].[AncestralTabletRegistrant] Script Date: 2025/10/29 下午 01:32:57 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[AncestralTabletRegistrant](
[RegistrantCode] [nvarchar](20) NOT NULL,
[Name] [nvarchar](50) NOT NULL,
[Phone] [nvarchar](50) NULL,
[Address] [nvarchar](60) NULL,
[RegisterDate] [date] NOT NULL,
[Price] [int] NULL,
[PositionId] [int] NULL,
[StartDate] [date] NOT NULL,
[EndDate] [date] NULL,
[IsLongTerm] [bit] NOT NULL,
[IsActive] [bit] NOT NULL,
[CreatedAt] [datetime] NOT NULL,
[UpdatedAt] [datetime] NULL,
[IsEnd] [bit] NOT NULL,
PRIMARY KEY CLUSTERED
(
[RegistrantCode] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object: Table [dbo].[AncestralTabletStatus] Script Date: 2025/10/29 下午 01:32:57 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[AncestralTabletStatus](
[StatusCode] [nvarchar](20) NOT NULL,
[StatusName] [nvarchar](20) NOT NULL,
[StatusType] [nvarchar](20) NOT NULL,
PRIMARY KEY CLUSTERED
(
[StatusCode] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[AncestralTabletArea] ADD DEFAULT ((0)) FOR [IsDisabled]
GO
ALTER TABLE [dbo].[AncestralTabletPositionRecord] ADD DEFAULT (getdate()) FOR [CreatedAt]
GO
ALTER TABLE [dbo].[AncestralTabletRegistrant] ADD DEFAULT ((0)) FOR [IsLongTerm]
GO
ALTER TABLE [dbo].[AncestralTabletRegistrant] ADD DEFAULT ((1)) FOR [IsActive]
GO
ALTER TABLE [dbo].[AncestralTabletRegistrant] ADD DEFAULT (getdate()) FOR [CreatedAt]
GO
ALTER TABLE [dbo].[AncestralTabletRegistrant] ADD DEFAULT ((0)) FOR [IsEnd]
GO
ALTER TABLE [dbo].[AncestralTabletArea] WITH CHECK ADD CONSTRAINT [FK_AncestralTabletArea_Parent] FOREIGN KEY([ParentAreaId])
REFERENCES [dbo].[AncestralTabletArea] ([AreaId])
GO
ALTER TABLE [dbo].[AncestralTabletArea] CHECK CONSTRAINT [FK_AncestralTabletArea_Parent]
GO
ALTER TABLE [dbo].[AncestralTabletPosition] WITH CHECK ADD CONSTRAINT [FK_Position_Area] FOREIGN KEY([AreaId])
REFERENCES [dbo].[AncestralTabletArea] ([AreaId])
GO
ALTER TABLE [dbo].[AncestralTabletPosition] CHECK CONSTRAINT [FK_Position_Area]
GO
ALTER TABLE [dbo].[AncestralTabletPosition] WITH CHECK ADD CONSTRAINT [FK_Position_Status] FOREIGN KEY([StatusCode])
REFERENCES [dbo].[AncestralTabletStatus] ([StatusCode])
GO
ALTER TABLE [dbo].[AncestralTabletPosition] CHECK CONSTRAINT [FK_Position_Status]
GO
ALTER TABLE [dbo].[AncestralTabletPositionRecord] WITH CHECK ADD FOREIGN KEY([RegistrantCode])
REFERENCES [dbo].[AncestralTabletRegistrant] ([RegistrantCode])
GO
ALTER TABLE [dbo].[AncestralTabletRegistrant] WITH CHECK ADD CONSTRAINT [FK_Registrant_Position] FOREIGN KEY([PositionId])
REFERENCES [dbo].[AncestralTabletPosition] ([PositionId])
GO
ALTER TABLE [dbo].[AncestralTabletRegistrant] CHECK CONSTRAINT [FK_Registrant_Position]
GO
INSERT [dbo].[AncestralTabletStatus] ([StatusCode], [StatusName], [StatusType]) VALUES (N'available', N'可用', N'Position')
INSERT [dbo].[AncestralTabletStatus] ([StatusCode], [StatusName], [StatusType]) VALUES (N'maintenance', N'維護中', N'Position')
INSERT [dbo].[AncestralTabletStatus] ([StatusCode], [StatusName], [StatusType]) VALUES (N'used', N'已使用', N'Position')
GO
-- 1. 如果不存在 OrderUuid 栏位,则新增
IF NOT EXISTS (
SELECT 1
FROM sys.columns
WHERE Name = N'OrderUuid'
AND Object_ID = Object_ID(N'dbo.GuaDanOrderGuest')
)
BEGIN
ALTER TABLE [dbo].[GuaDanOrderGuest]
ADD [OrderUuid] UNIQUEIDENTIFIER NULL;
END
GO
-- 2. 如果不存在 FK_GuaDanOrderGuest_Order 外键,则新增
IF NOT EXISTS (
SELECT 1
FROM sys.foreign_keys
WHERE Name = N'FK_GuaDanOrderGuest_Order'
AND parent_object_id = OBJECT_ID(N'dbo.GuaDanOrderGuest')
)
BEGIN
ALTER TABLE [dbo].[GuaDanOrderGuest]
ADD CONSTRAINT [FK_GuaDanOrderGuest_Order]
FOREIGN KEY ([OrderUuid]) REFERENCES [dbo].[GuaDanOrder] ([Uuid]);
END
GO

26
data/memo/report-view.md Normal file
View File

@@ -0,0 +1,26 @@
# 報表系統規劃
為每場活動(法會)建立報名到舉辦過程的報表,
要涵蓋以下面向的統計分析資訊
- 以單一場法會為核心
- 時間面向: 當前狀況, 指定期間狀況
- 維度:
- 信眾: 報名數量, 金額, 收款狀態
- 牌位型態(活動品項表): 報名數量, 金額, 收款狀態
- 收款狀態: 己收/未收 統計明細
## 法會報表系統查詢規劃**
### 1. 核心基礎 VIEW
1. `vw_activity_registration_base` - 單一活動的完整報名基礎資料
2. `vw_activity_payment_detail` - 單一活動的完整收款明細資料
### 2. 統計分析 VIEW
3. `vw_activity_follower_statistics` - 按信眾統計報名情況
4. `vw_activity_item_statistics` - 按品項統計報名情況
5. `vw_activity_payment_status` - 收款狀態統計分析
### 3. 時間維度分析 VIEW
6. `vw_activity_registration_trend` - 按日期統計報名趨勢
7. `vw_activity_payment_trend` - 按收款日期統計收款趨勢
### 4. 詳細查詢 VIEW
8. `vw_activity_unpaid_detail` - 未收款明細清單
9. `vw_activity_transfer_reconciliation` - 匯款對帳明細

252
data/memo/report-view.sql Normal file
View File

@@ -0,0 +1,252 @@
drop view if exists vw_activity_registration_base;
drop view if exists vw_activity_payment_detail;
drop view if exists vw_activity_follower_statistics;
drop view if exists vw_activity_item_statistics;
drop view if exists vw_activity_payment_status;
drop view if exists vw_activity_registration_trend;
drop view if exists vw_activity_payment_trend;
drop view if exists vw_activity_unpaid_detail;
drop view if exists vw_activity_transfer_reconciliation;
GO
-- 1. 法會報名基礎資料 VIEW
CREATE VIEW vw_activity_registration_base AS
SELECT
a.num AS ,
a.subject AS ,
a.startDate_solar AS ,
a.endDate_solar AS ,
po.order_no AS ,
po.up_time AS ,
po.keyin1 AS ,
f.num AS ,
f.f_number AS ,
f.u_name AS ,
f.phone AS ,
f.identity_type AS ,
f.country AS ,
pod.num AS ,
ai.num AS ,
ai.subject AS ,
ai.category AS ,
pod.price AS ,
pod.qty AS ,
pod.price * pod.qty AS ,
pod.pay AS ,
(pod.price * pod.qty - ISNULL(pod.pay, 0)) AS ,
pod.pay_date AS ,
pod.start_date AS ,
pod.due_date AS ,
pod.keyin1 AS ,
pod.demo AS
FROM activity a
INNER JOIN pro_order po ON a.num = po.activity_num
INNER JOIN followers f ON po.f_num = f.num
INNER JOIN pro_order_detail pod ON po.order_no = pod.order_no
INNER JOIN actItem ai ON pod.actItem_num = ai.num;
GO
-- 2. 收款明細基礎資料 VIEW
CREATE VIEW vw_activity_payment_detail AS
SELECT
a.num AS ,
a.subject AS ,
po.order_no AS ,
f.u_name AS ,
pod.num AS ,
ai.subject AS ,
por.num AS ,
por.price AS ,
por.payment AS ,
por.pay_date AS ,
por.organization AS ,
por.bank_code AS ,
por.transfer_id AS ID,
por.reconcile_memo AS ,
tr.name AS ,
tr.phone AS ,
tr.amount AS ,
tr.check_date AS ,
tr.status AS
FROM activity a
INNER JOIN pro_order po ON a.num = po.activity_num
INNER JOIN followers f ON po.f_num = f.num
INNER JOIN pro_order_detail pod ON po.order_no = pod.order_no
INNER JOIN actItem ai ON pod.actItem_num = ai.num
LEFT JOIN pro_order_record por ON pod.num = por.detail_num
LEFT JOIN transfer_register tr ON por.transfer_id = tr.id;
GO
-- 3. 信眾報名統計 VIEW
CREATE VIEW vw_activity_follower_statistics AS
SELECT
,
,
,
,
,
,
,
,
COUNT(DISTINCT ) AS ,
COUNT() AS ,
SUM() AS ,
SUM() AS ,
SUM() AS ,
CASE
WHEN SUM() = 0 THEN '已繳清'
WHEN SUM() = SUM() THEN '未繳'
ELSE '部分繳款'
END AS
FROM vw_activity_registration_base
GROUP BY , , , , , , , ;
GO
-- 4. 品項報名統計 VIEW
CREATE VIEW vw_activity_item_statistics AS
SELECT
,
,
,
,
,
,
COUNT() AS ,
SUM() AS ,
SUM() AS ,
SUM() AS ,
SUM() AS ,
CASE
WHEN COUNT() = 0 THEN 0
ELSE AVG()
END AS ,
CASE
WHEN SUM() = 0 THEN '已收齊'
WHEN SUM() = SUM() THEN '未收款'
ELSE '部分收款'
END AS
FROM vw_activity_registration_base
GROUP BY , , , , , ;
GO
-- 5. 收款狀態統計 VIEW
CREATE VIEW vw_activity_payment_status AS
SELECT
,
,
'總計' AS ,
COUNT(DISTINCT ) AS ,
COUNT() AS ,
SUM() AS ,
SUM() AS ,
SUM() AS ,
CASE
WHEN SUM() = 0 THEN 0
ELSE ROUND(SUM() * 100.0 / SUM(), 2)
END AS
FROM vw_activity_registration_base
GROUP BY ,
UNION ALL
SELECT
,
,
CASE
WHEN = 0 THEN '已繳清'
WHEN = THEN '未繳'
ELSE '部分繳款'
END AS ,
COUNT(DISTINCT ) AS ,
COUNT() AS ,
SUM() AS ,
SUM() AS ,
SUM() AS ,
CASE
WHEN SUM() = 0 THEN 0
ELSE ROUND(SUM() * 100.0 / SUM(), 2)
END AS
FROM vw_activity_registration_base
GROUP BY , ,
CASE
WHEN = 0 THEN '已繳清'
WHEN = THEN '未繳'
ELSE '部分繳款'
END;
GO
-- 6. 報名趨勢分析 VIEW
CREATE VIEW vw_activity_registration_trend AS
SELECT
,
,
CAST( AS DATE) AS ,
COUNT(DISTINCT ) AS ,
COUNT() AS ,
SUM() AS ,
SUM() AS ,
SUM() AS
FROM vw_activity_registration_base
GROUP BY , , CAST( AS DATE);
GO
-- 7. 收款趨勢分析 VIEW
CREATE VIEW vw_activity_payment_trend AS
SELECT
,
,
CAST( AS DATE) AS ,
COUNT() AS ,
SUM() AS ,
COUNT(DISTINCT ) AS ,
CASE
WHEN COUNT() = 0 THEN 0
ELSE AVG()
END AS
FROM vw_activity_payment_detail
WHERE IS NOT NULL
GROUP BY , , CAST( AS DATE);
GO
-- 8. 未收款明細 VIEW
CREATE VIEW vw_activity_unpaid_detail AS
SELECT
,
,
,
,
,
,
,
,
,
,
CASE
WHEN < GETDATE() THEN '已逾期'
WHEN <= DATEADD(DAY, 3, GETDATE()) THEN '即將到期'
ELSE '未到期'
END AS
FROM vw_activity_registration_base
WHERE > 0;
GO
-- 9. 匯款對帳明細 VIEW
CREATE VIEW vw_activity_transfer_reconciliation AS
SELECT
,
,
ID,
,
,
,
,
,
COUNT() AS ,
SUM() AS ,
CASE
WHEN SUM() IS NULL THEN
ELSE - SUM()
END AS
FROM vw_activity_payment_detail
WHERE ID IS NOT NULL
GROUP BY , , ID, , , , , ;
GO

484
data/memo/report.md Normal file
View File

@@ -0,0 +1,484 @@
# 相關頁面
## 基本功能
admin/order/index.aspx
admin/activity/index.aspx
admin/follower/index.aspx
admin/activity/index2.aspx
admin/transfer/index.aspx
## 入帳沖帳
D:\dev\ez\17168erp\git_17888\web\admin\transfer\balance_reconcile_query.aspx
D:\dev\ez\17168erp\git_17888\web\admin\transfer\balance_reconcile.aspx
D:\dev\ez\17168erp\git_17888\web\admin\transfer\group_reconcile.aspx
D:\dev\ez\17168erp\git_17888\web\admin\transfer\index.aspx
D:\dev\ez\17168erp\git_17888\web\admin\transfer\personal_reconcile.aspx
D:\dev\ez\17168erp\git_17888\web\admin\transfer\register.aspx
D:\dev\ez\17168erp\git_17888\web\admin\transfer\verify_order_record_query.aspx
D:\dev\ez\17168erp\git_17888\web\admin\transfer\verify.aspx
D:\dev\ez\17168erp\git_17888\web\admin\transfer\verify1.aspx
D:\dev\ez\17168erp\git_17888\web\admin\transfer\verify2.aspx
# 資料結構
## 📊 17168ERP 系統使用的資料表架構
### 🎯 **核心業務資料表**
#### 1. **報名管理系統** (`order/index.aspx`)
**主要資料表:**
- **`pro_order`** - 報名主表
- `order_no` (單號)、`up_time` (報名日期)、`keyin1` (單據狀態)
- `f_num` (信眾編號)、`activity_num` (活動編號)、`phone` (聯絡電話)
- **`pro_order_detail`** - 報名明細表
- `order_no` (關聯主表)、`actItem_num` (活動品項)、`f_num` (報名者)
- `price` (金額)、`qty` (數量)、`pay` (已收金額)、`pay_date` (付款期限)
- **`activity`** - 活動主表
- `num``subject` (活動名稱)、`start_date` (開始日期)、`end_date` (結束日期)
- **`actItem`** - 活動品項表
- `num``subject` (品項名稱)、`category` (品項分類)
#### 2. **信眾管理系統** (`follower/index.aspx`)
**主要資料表:**
- **`followers`** - 信眾基本資料表
- `num``f_number` (信眾編號)、`u_name` (姓名)、`sex` (性別)
- `identity_type` (身分別)、`birthday` (生日)、`phone` (電話)
- `address` (地址)、`country` (國籍)、`refugedate` (皈依日期)
- **`countries`** - 國籍資料表
- `ID``name_zh` (中文名稱)、`name_en` (英文名稱)
#### 3. **活動管理系統** (`activity/index.aspx`)
**主要資料表:**
- **`activity`** - 活動主表
- `num``subject` (活動名稱)、`startDate_solar` (國曆開始日期)
- `startDate_lunar` (農曆開始日期)、`endDate_solar` (國曆結束日期)
- `endDate_lunar` (農曆結束日期)、`dueDate` (報名截止日期)
- **`activity_kind`** - 活動分類表
- `num``subject` (分類名稱)
#### 4. **匯款沖帳系統** (`transfer/index.aspx`)
**主要資料表:**
- **`transfer_register`** - 匯款登錄表
- `id``name` (匯款人姓名)、`phone` (電話)、`amount` (匯款金額)
- `pay_type` (付款方式)、`account_last5` (帳號後五碼)
- `proof_img` (匯款證明圖片)、`status` (狀態)
- `f_num_match` (配對信眾編號)、`check_amount` (核對金額)
- **`accounting`** - 會計帳務表
- `num``category` (科目分類)、`kind` (收支類型)
- `price` (金額)、`debtor` (債務人)、`activity_num` (關聯活動)
- **`pro_order_record`** - 報名收款記錄表
- `num``detail_num` (關聯明細)、`price` (金額)、`payment` (付款方式)
- `pay_date` (收款日期)、`transfer_id` (關聯匯款記錄)
### 🔗 **關聯關係**
#### **主要外鍵關聯:**
```
pro_order → followers (f_num)
pro_order → activity (activity_num)
pro_order_detail → pro_order (order_no)
pro_order_detail → actItem (actItem_num)
pro_order_detail → followers (f_num)
transfer_register → followers (f_num)
transfer_register → activity (activity_num)
accounting → pro_order_detail (pro_order_detail_num)
```
### 📈 **資料表統計**
| 功能模組 | 主要資料表數量 | 核心業務表 |
|---------|-------------|-----------|
| 報名管理 | 5個 | `pro_order`, `pro_order_detail` |
| 信眾管理 | 2個 | `followers`, `countries` |
| 活動管理 | 2個 | `activity`, `activity_kind` |
| 匯款沖帳 | 3個 | `transfer_register`, `accounting`, `pro_order_record` |
### 🎯 **系統特色**
1. **模組化設計**:每個功能模組都有獨立的資料表群組
2. **關聯完整**:透過外鍵建立完整的業務關聯關係
3. **狀態管理**:多個狀態表支援複雜的業務流程
4. **UUID設計**新模組採用UUID作為主鍵提升擴展性
5. **軟刪除機制**:使用`IsDeleted`欄位而非物理刪除
這個資料表架構支援了一個完整的宗教活動管理ERP系統涵蓋了從信眾管理、活動報名、床位安排到財務沖帳的全流程管理。
# 入帳沖帳
```
報名者匯款 → transfer_register (status: pending)
出納核對身份 → transfer_register (status: verified, f_num_match)
出納核對金額 → transfer_register (status: confirmed, check_amount)
會計個人沖帳 → pro_order_record + accounting
會計共同沖帳 → pro_order_record + accounting (多筆)
會計餘額核銷 → transfer_register (remain_amount)
查詢統計報表 → 各表關聯查詢
```
# DB SCHEMA
我來為您使用 Mermaid 語法繪製 17168ERP 系統的 ER 圖,展示各個資料表之間的關聯關係。
[1 tool called]
我無法直接創建 Mermaid 圖表,但我可以為您提供完整的 Mermaid ER 圖代碼,您可以複製到支援 Mermaid 的工具中查看(如 GitHub、GitLab、或 Mermaid Live Editor
以下是 17168ERP 系統的 ER 圖 Mermaid 代碼:
```mermaid
erDiagram
%% 信眾管理系統
followers {
int num PK
string f_number
string u_name
string sex
int identity_type
datetime birthday
string phone
string email
datetime refugedate
string refuge_name
string address
string demo
int leader
string socialid1
string socialid2
string tab
string contactor
string contactor_phone
string blood
string customize_data
}
countries {
int ID PK
string name_zh
string name_en
string range
}
%% 活動管理系統
activity {
int num PK
string subject
datetime startDate_solar
datetime startDate_lunar
datetime endDate_solar
datetime endDate_lunar
datetime dueDate
int kind
string demo
string customize_data
}
activity_kind {
int num PK
string subject
string demo
}
actItem {
int num PK
int activity_num FK
string subject
int category
string demo
string customize_data
}
%% 報名管理系統
pro_order {
string order_no PK
datetime up_time
datetime reg_time
string keyin1
int f_num FK
string phone
int activity_num FK
string address
string demo
string customize_data
int introducer FK
boolean send_receipt
string receipt_title
}
pro_order_detail {
int num PK
string order_no FK
int actItem_num FK
int f_num FK
string f_num_tablet
string address
int from_id FK
string from_id_tablet
datetime due_date
int bed_type
float price
int qty
datetime start_date
datetime extend_date
float pay
datetime pay_date
int keyin1
string demo
datetime UpdateTime
}
%% 匯款沖帳系統
transfer_register {
int id PK
int activity_num FK
string name
string phone
string pay_type
string account_last5
decimal amount
string pay_mode
string note
string proof_img
string status
datetime create_time
int f_num_match FK
int f_num FK
int acc_num
datetime check_date
decimal check_amount
string check_memo
string check_status
int acc_kind
int member_num
datetime verify_time
string verify_note
string draft
decimal remain_amount
int balance_act_item FK
int balance_pro_order_detail FK
}
accounting {
int num PK
datetime uptime
int category
int kind
int kind2
float price
float tax
string demo
int mem_num
string debtor
int activity_num FK
string excerpt
datetime reg_time
int pro_order_detail_num FK
}
pro_order_record {
int num PK
int detail_num FK
float price
int payment
datetime reg_time
datetime pay_date
string organization
string bank_code
int transfer_id FK
string reconcile_memo
}
%% 區域床位管理系統
Region {
Guid Uuid PK
string Name
boolean Gender
boolean IsActive
boolean IsDeleted
}
Room {
Guid Uuid PK
string Name
boolean Gender
int BedCount
boolean IsActive
datetime CreatedAt
datetime UpdatedAt
boolean IsDeleted
Guid RegionUuid FK
}
RegionRoomBed {
Guid Uuid PK
string Name
boolean IsActive
boolean Gender
boolean IsDeleted
Guid RoomUuid FK
string StatusCode FK
}
RegionRoomBedStatus {
string Code PK
string Name
string Description
int Category
boolean IsDeleted
}
%% 掛單管理系統
GuaDanOrder {
Guid Uuid PK
datetime StartDate
datetime EndDate
int CreateUser FK
datetime CreatedAt
datetime UpdatedAt
string Notes
string GuaDanOrderNo
int BookerFollowerNum FK
string BookerName
string BookerPhone
boolean IsDeleted
int ActivityNum FK
boolean IsCancel
}
GuaDanOrderGuest {
Guid Uuid PK
string GuaDanOrderNo FK
int FollowerNum FK
boolean IsDeleted
Guid RoomUuid FK
Guid BedUuid FK
datetime CheckInAt
datetime CheckOutAt
string StatusCode FK
}
RegionAndRoomAndBedSchedule {
Guid Uuid PK
Guid TargetUuid FK
string GuaDanOrderNo
date ScheduleDate
boolean IsCancel
boolean IsDeleted
int UseType
string Title
string Description
string CreatedBy
datetime CreatedAt
Guid GuaDanOrderGuestUuid FK
}
%% 關聯關係
followers ||--o{ pro_order : "報名"
followers ||--o{ pro_order_detail : "報名明細"
followers ||--o{ transfer_register : "匯款人"
followers ||--o{ GuaDanOrderGuest : "掛單客人"
activity ||--o{ pro_order : "活動報名"
activity ||--o{ actItem : "活動品項"
activity ||--o{ transfer_register : "活動匯款"
activity ||--o{ GuaDanOrder : "掛單活動"
pro_order ||--o{ pro_order_detail : "報名明細"
pro_order ||--o{ pro_order_record : "收款記錄"
pro_order_detail ||--o{ pro_order_record : "明細收款"
pro_order_detail ||--o{ accounting : "會計分錄"
pro_order_detail ||--o{ transfer_register : "沖帳明細"
transfer_register ||--o{ pro_order_record : "匯款記錄"
transfer_register ||--o{ accounting : "會計記錄"
Region ||--o{ Room : "區域房間"
Room ||--o{ RegionRoomBed : "房間床位"
RegionRoomBedStatus ||--o{ RegionRoomBed : "床位狀態"
RegionRoomBedStatus ||--o{ GuaDanOrderGuest : "客人狀態"
GuaDanOrder ||--o{ GuaDanOrderGuest : "掛單客人"
GuaDanOrderGuest ||--o{ RegionAndRoomAndBedSchedule : "床位排程"
countries ||--o{ followers : "國籍"
activity_kind ||--o{ activity : "活動分類"
```
## 📊 **ER 圖說明**
### **主要實體群組:**
1. **信眾管理群組**
- `followers` (信眾基本資料)
- `countries` (國籍資料)
2. **活動管理群組**
- `activity` (活動主表)
- `activity_kind` (活動分類)
- `actItem` (活動品項)
3. **報名管理群組**
- `pro_order` (報名主表)
- `pro_order_detail` (報名明細)
4. **財務管理群組**
- `transfer_register` (匯款登錄)
- `accounting` (會計帳務)
- `pro_order_record` (收款記錄)
5. **床位管理群組**
- `Region` (區域)
- `Room` (房間)
- `RegionRoomBed` (床位)
- `RegionRoomBedStatus` (床位狀態)
6. **掛單管理群組**
- `GuaDanOrder` (掛單主表)
- `GuaDanOrderGuest` (掛單客人)
- `RegionAndRoomAndBedSchedule` (床位排程)
### **關鍵關聯關係:**
- **一對多關係**:使用 `||--o{` 表示
- **外鍵關聯**FK 標記顯示主要的外鍵關聯
- **業務流程**:從信眾報名 → 匯款 → 沖帳 → 床位安排的完整流程
您可以将這個 Mermaid 代碼複製到 [Mermaid Live Editor](https://mermaid.live/) 或其他支援 Mermaid 的工具中查看完整的 ER 圖表。
---
# 報表系統規劃
為每場活動(法會)建立報名到舉辦過程的報表,
要涵蓋以下面向的統計分析資訊
- 以單一場法會為核心
- 時間面向: 當前狀況, 指定期間狀況
- 維度:
- 信眾: 報名數量, 金額, 收款狀態
- 牌位型態(活動品項表): 報名數量, 金額, 收款狀態
- 收款狀態: 己收/未收 統計明細
## 執行方式:
- 類似excel, 詳細資料->pivot table
- 先建立一個(或數個)最核心的sql view, 包含各項:報名資料, 收款明細
- 先以單一活動編號為固定FILTER : activity.num=59
- 再依不同面向, 建立第二級的sql view
- 再人工將以上:第一, 第二級的SQL VIEW, 以EXCEL查詢, 做資料分析/整理
- 相關英文欄名, 在VIEW中以中文別名顯示
## 相關SQL VIEW
- (查詢清單)
### (查詢)
(說明)
```sql
```

View File

@@ -0,0 +1,11 @@
資料字典:
USE [17168erp_t]
GO
INSERT [dbo].[AncestralTabletStatus] ([StatusCode], [StatusName], [StatusType]) VALUES (N'available', N'可用', N'Position')
INSERT [dbo].[AncestralTabletStatus] ([StatusCode], [StatusName], [StatusType]) VALUES (N'maintenance', N'維護中', N'Position')
INSERT [dbo].[AncestralTabletStatus] ([StatusCode], [StatusName], [StatusType]) VALUES (N'used', N'已使用', N'Position')
GO
代碼中會用到上面表中的狀態,所以必須在該表中插入上面的數據
AncestralTabletStatus_script.sql文件中的表是神祖牌位功能模組需要用到的表再運行之前需要先執行該文件
執行AncestralTabletStatus_script.sql後要把在item表中新增的URL權限添加到相應的管理員帳號

BIN
data/查詢範例.xlsx Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
/// <summary>
/// GuaDanStatusCode 的摘要描述
/// </summary>
public static class GuaDanStatusCode
{
public static class Bed
{
public const string Empty = "101"; // 空床:床位可分配
public const string Occupied = "102"; // 占用中:床位已有人使用
public const string Repair = "103"; // 維修停用:床位維修或不可使用
}
public static class Room
{
public const string Empty = "301"; // 空房:房間所有床位皆為空
public const string Partly = "302"; // 部分入住:房間有人,但仍有空床
public const string Full = "303"; // 已滿:房間所有床位皆已入住
public const string Repair = "304"; // 維修停用:房間維修或不可使用
}
public static class Guadan
{
public const string Booked = "401"; // 預訂成功:默認就是預訂成功狀態
public const string CheckedIn = "402"; // 已入住:已辦理入住
public const string CheckedOut = "403"; // 已退房
public const string Cancelled = "404"; // 已取消:取消後的狀態,不是取消的動作
}
}

View File

@@ -1,9 +1,9 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated from a template.
// 這個程式碼是由範本產生。
//
// Manual changes to this file may cause unexpected behavior in your application.
// Manual changes to this file will be overwritten if the code is regenerated.
// 對這個檔案進行手動變更可能導致您的應用程式產生未預期的行為。
// 如果重新產生程式碼,將會覆寫對這個檔案的手動變更。
// </auto-generated>
//------------------------------------------------------------------------------
@@ -74,7 +74,6 @@ namespace Model
public virtual DbSet<stock_reason> stock_reason { get; set; }
public virtual DbSet<supplier> suppliers { get; set; }
public virtual DbSet<supplier_kind> supplier_kind { get; set; }
public virtual DbSet<sysdiagram> sysdiagrams { get; set; }
public virtual DbSet<act_bom> act_bom { get; set; }
public virtual DbSet<family_members> family_members { get; set; }
public virtual DbSet<PostCity> PostCitiy { get; set; }
@@ -90,6 +89,11 @@ namespace Model
public virtual DbSet<RegionRoomBedStatus> RegionRoomBedStatus { get; set; }
public virtual DbSet<RegionType> RegionType { get; set; }
public virtual DbSet<Room> Room { get; set; }
public virtual DbSet<AncestralTabletArea> AncestralTabletArea { get; set; }
public virtual DbSet<AncestralTabletPosition> AncestralTabletPosition { get; set; }
public virtual DbSet<AncestralTabletPositionRecord> AncestralTabletPositionRecord { get; set; }
public virtual DbSet<AncestralTabletRegistrant> AncestralTabletRegistrant { get; set; }
public virtual DbSet<AncestralTabletStatus> AncestralTabletStatus { get; set; }
public virtual int pager_eztrust(Nullable<int> startRowIndex, Nullable<int> pageSize, string tableName, string columnName, string sqlWhere, string orderBy, ObjectParameter rowCount)
{

View File

@@ -1,10 +1,10 @@
// T4 code generation is enabled for model 'D:\dev\ez\17168erp\git_17888\web\App_Code\Model\Model.edmx'.
// To enable legacy code generation, change the value of the 'Code Generation Strategy' designer
// property to 'Legacy ObjectContext'. This property is available in the Properties Window when the model
// is open in the designer.
// 已啟用模型 'D:\17168erp_new_git\17168ERP\web\App_Code\Model\Model.edmx' 的 T4 程式碼產生。
// 若要啟用舊版程式碼產生,請將 [程式碼產生策略] 設計工具屬性的值
//變更為 [舊版 ObjectContext]。當模型在設計工具中開啟時,這個屬性便可
//以在 [屬性] 視窗中使用。
// If no context and entity classes have been generated, it may be because you created an empty model but
// have not yet chosen which version of Entity Framework to use. To generate a context class and entity
// classes for your model, open the model in the designer, right-click on the designer surface, and
// select 'Update Model from Database...', 'Generate Database from Model...', or 'Add Code Generation
// Item...'.
// 如果尚未產生任何內容和實體類型,可能是因為您建立了空的模型,但
//尚未選擇要使用的 Entity Framework 版本。若要為您的模型產生內容類別和
//實體類型,請在設計工具中開啟模型,以滑鼠右鍵按一下設計工具介面並
//選取 [從資料庫更新模型]、[由模型產生資料庫] 或 [加入程式碼產生
//項目]。

View File

@@ -1,9 +1,9 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated from a template.
// 這個程式碼是由範本產生。
//
// Manual changes to this file may cause unexpected behavior in your application.
// Manual changes to this file will be overwritten if the code is regenerated.
// 對這個檔案進行手動變更可能導致您的應用程式產生未預期的行為。
// 如果重新產生程式碼,將會覆寫對這個檔案的手動變更。
// </auto-generated>
//------------------------------------------------------------------------------
@@ -441,7 +441,7 @@ namespace Model
{
this.members = new HashSet<member>();
this.news = new HashSet<news>();
this.GuaDanOrders = new HashSet<GuaDanOrder>();
this.GuaDanOrder = new HashSet<GuaDanOrder>();
}
public int num { get; set; }
@@ -477,7 +477,7 @@ namespace Model
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<news> news { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<GuaDanOrder> GuaDanOrders { get; set; }
public virtual ICollection<GuaDanOrder> GuaDanOrder { get; set; }
}
}
namespace Model
@@ -523,6 +523,139 @@ namespace Model
using System;
using System.Collections.Generic;
public partial class AncestralTabletArea
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public AncestralTabletArea()
{
this.AncestralTabletArea1 = new HashSet<AncestralTabletArea>();
this.AncestralTabletPosition = new HashSet<AncestralTabletPosition>();
}
public int AreaId { get; set; }
public string AreaName { get; set; }
public string AreaCode { get; set; }
public Nullable<int> ParentAreaId { get; set; }
public string AreaType { get; set; }
public Nullable<int> Price { get; set; }
public Nullable<int> SortOrder { get; set; }
public bool IsDisabled { get; set; }
public string Description { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<AncestralTabletArea> AncestralTabletArea1 { get; set; }
public virtual AncestralTabletArea AncestralTabletArea2 { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<AncestralTabletPosition> AncestralTabletPosition { get; set; }
}
}
namespace Model
{
using System;
using System.Collections.Generic;
public partial class AncestralTabletPosition
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public AncestralTabletPosition()
{
this.AncestralTabletRegistrant = new HashSet<AncestralTabletRegistrant>();
}
public int PositionId { get; set; }
public int AreaId { get; set; }
public string PositionCode { get; set; }
public string PositionName { get; set; }
public Nullable<int> Price { get; set; }
public string StatusCode { get; set; }
public string Description { get; set; }
public Nullable<int> RowNo { get; set; }
public Nullable<int> ColumnNo { get; set; }
public virtual AncestralTabletArea AncestralTabletArea { get; set; }
public virtual AncestralTabletStatus AncestralTabletStatus { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<AncestralTabletRegistrant> AncestralTabletRegistrant { get; set; }
}
}
namespace Model
{
using System;
using System.Collections.Generic;
public partial class AncestralTabletPositionRecord
{
public int RecordId { get; set; }
public string RegistrantCode { get; set; }
public string NPTitle { get; set; }
public System.DateTime NPStandDate { get; set; }
public string NPYangShang { get; set; }
public string WPContent { get; set; }
public System.DateTime CreatedAt { get; set; }
public Nullable<System.DateTime> UpdatedAt { get; set; }
public virtual AncestralTabletRegistrant AncestralTabletRegistrant { get; set; }
}
}
namespace Model
{
using System;
using System.Collections.Generic;
public partial class AncestralTabletRegistrant
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public AncestralTabletRegistrant()
{
this.AncestralTabletPositionRecord = new HashSet<AncestralTabletPositionRecord>();
}
public string RegistrantCode { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
public string Address { get; set; }
public System.DateTime RegisterDate { get; set; }
public Nullable<int> Price { get; set; }
public Nullable<int> PositionId { get; set; }
public System.DateTime StartDate { get; set; }
public Nullable<System.DateTime> EndDate { get; set; }
public bool IsLongTerm { get; set; }
public bool IsActive { get; set; }
public System.DateTime CreatedAt { get; set; }
public Nullable<System.DateTime> UpdatedAt { get; set; }
public bool IsEnd { get; set; }
public virtual AncestralTabletPosition AncestralTabletPosition { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<AncestralTabletPositionRecord> AncestralTabletPositionRecord { get; set; }
}
}
namespace Model
{
using System;
using System.Collections.Generic;
public partial class AncestralTabletStatus
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public AncestralTabletStatus()
{
this.AncestralTabletPosition = new HashSet<AncestralTabletPosition>();
}
public string StatusCode { get; set; }
public string StatusName { get; set; }
public string StatusType { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<AncestralTabletPosition> AncestralTabletPosition { get; set; }
}
}
namespace Model
{
using System;
using System.Collections.Generic;
public partial class appellation
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
@@ -768,8 +901,8 @@ namespace Model
this.family_members = new HashSet<family_members>();
this.transfer_register = new HashSet<transfer_register>();
this.transfer_register1 = new HashSet<transfer_register>();
this.GuaDanOrders = new HashSet<GuaDanOrder>();
this.GuaDanOrderGuests = new HashSet<GuaDanOrderGuest>();
this.GuaDanOrder = new HashSet<GuaDanOrder>();
this.GuaDanOrderGuest = new HashSet<GuaDanOrderGuest>();
}
public int num { get; set; }
@@ -830,9 +963,9 @@ namespace Model
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<transfer_register> transfer_register1 { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<GuaDanOrder> GuaDanOrders { get; set; }
public virtual ICollection<GuaDanOrder> GuaDanOrder { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<GuaDanOrderGuest> GuaDanOrderGuests { get; set; }
public virtual ICollection<GuaDanOrderGuest> GuaDanOrderGuest { get; set; }
}
}
namespace Model
@@ -857,6 +990,12 @@ namespace Model
public partial class GuaDanOrder
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public GuaDanOrder()
{
this.GuaDanOrderGuest = new HashSet<GuaDanOrderGuest>();
}
public Nullable<System.DateTime> StartDate { get; set; }
public Nullable<System.DateTime> EndDate { get; set; }
public Nullable<int> CreateUser { get; set; }
@@ -869,9 +1008,13 @@ namespace Model
public string BookerPhone { get; set; }
public bool IsDeleted { get; set; }
public System.Guid Uuid { get; set; }
public Nullable<int> ActivityNum { get; set; }
public bool IsCancel { get; set; }
public virtual admin admin { get; set; }
public virtual follower follower { get; set; }
public virtual follower followers { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<GuaDanOrderGuest> GuaDanOrderGuest { get; set; }
}
}
namespace Model
@@ -881,6 +1024,12 @@ namespace Model
public partial class GuaDanOrderGuest
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public GuaDanOrderGuest()
{
this.RegionAndRoomAndBedSchedule = new HashSet<RegionAndRoomAndBedSchedule>();
}
public string GuaDanOrderNo { get; set; }
public Nullable<int> FollowerNum { get; set; }
public bool IsDeleted { get; set; }
@@ -889,12 +1038,16 @@ namespace Model
public Nullable<System.Guid> BedUuid { get; set; }
public Nullable<System.DateTime> CheckInAt { get; set; }
public Nullable<System.DateTime> CheckOutAt { get; set; }
public Nullable<System.Guid> statusUuid { get; set; }
public string StatusCode { get; set; }
public Nullable<System.Guid> OrderUuid { get; set; }
public virtual follower follower { get; set; }
public virtual follower followers { get; set; }
public virtual RegionRoomBed RegionRoomBed { get; set; }
public virtual Room Room { get; set; }
public virtual RegionRoomBedStatus RegionRoomBedStatus { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<RegionAndRoomAndBedSchedule> RegionAndRoomAndBedSchedule { get; set; }
public virtual GuaDanOrder GuaDanOrder { get; set; }
}
}
namespace Model
@@ -1393,7 +1546,10 @@ namespace Model
public Nullable<System.Guid> TargetUuid { get; set; }
public string GuaDanOrderNo { get; set; }
public Nullable<System.DateTime> ScheduleDate { get; set; }
public bool IsActive { get; set; }
public bool IsCancel { get; set; }
public Nullable<System.Guid> GuaDanOrderGuestUuid { get; set; }
public virtual GuaDanOrderGuest GuaDanOrderGuest { get; set; }
}
}
namespace Model
@@ -1406,7 +1562,7 @@ namespace Model
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public RegionRoomBed()
{
this.GuaDanOrderGuests = new HashSet<GuaDanOrderGuest>();
this.GuaDanOrderGuest = new HashSet<GuaDanOrderGuest>();
}
public string Name { get; set; }
@@ -1415,12 +1571,12 @@ namespace Model
public bool IsDeleted { get; set; }
public System.Guid Uuid { get; set; }
public System.Guid RoomUuid { get; set; }
public Nullable<System.Guid> StatusUuid { get; set; }
public string StatusCode { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<GuaDanOrderGuest> GuaDanOrderGuests { get; set; }
public virtual RegionRoomBedStatus RegionRoomBedStatus { get; set; }
public virtual ICollection<GuaDanOrderGuest> GuaDanOrderGuest { get; set; }
public virtual Room Room { get; set; }
public virtual RegionRoomBedStatus RegionRoomBedStatus { get; set; }
}
}
namespace Model
@@ -1433,8 +1589,8 @@ namespace Model
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public RegionRoomBedStatus()
{
this.GuaDanOrderGuests = new HashSet<GuaDanOrderGuest>();
this.RegionRoomBeds = new HashSet<RegionRoomBed>();
this.GuaDanOrderGuest = new HashSet<GuaDanOrderGuest>();
this.RegionRoomBed = new HashSet<RegionRoomBed>();
}
public string Code { get; set; }
@@ -1442,12 +1598,11 @@ namespace Model
public string Description { get; set; }
public Nullable<int> Category { get; set; }
public bool IsDeleted { get; set; }
public System.Guid Uuid { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<GuaDanOrderGuest> GuaDanOrderGuests { get; set; }
public virtual ICollection<GuaDanOrderGuest> GuaDanOrderGuest { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<RegionRoomBed> RegionRoomBeds { get; set; }
public virtual ICollection<RegionRoomBed> RegionRoomBed { get; set; }
}
}
namespace Model
@@ -1460,7 +1615,7 @@ namespace Model
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public RegionType()
{
this.Regions = new HashSet<Region>();
this.Region = new HashSet<Region>();
}
public string Code { get; set; }
@@ -1471,7 +1626,7 @@ namespace Model
public System.Guid Uuid { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<Region> Regions { get; set; }
public virtual ICollection<Region> Region { get; set; }
}
}
namespace Model
@@ -1484,7 +1639,7 @@ namespace Model
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Room()
{
this.GuaDanOrderGuests = new HashSet<GuaDanOrderGuest>();
this.GuaDanOrderGuest = new HashSet<GuaDanOrderGuest>();
this.RegionRoomBed = new HashSet<RegionRoomBed>();
}
@@ -1499,7 +1654,7 @@ namespace Model
public System.Guid RegionUuid { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<GuaDanOrderGuest> GuaDanOrderGuests { get; set; }
public virtual ICollection<GuaDanOrderGuest> GuaDanOrderGuest { get; set; }
public virtual Region Region { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<RegionRoomBed> RegionRoomBed { get; set; }
@@ -1685,20 +1840,6 @@ namespace Model
using System;
using System.Collections.Generic;
public partial class sysdiagram
{
public string name { get; set; }
public int principal_id { get; set; }
public int diagram_id { get; set; }
public Nullable<int> version { get; set; }
public byte[] definition { get; set; }
}
}
namespace Model
{
using System;
using System.Collections.Generic;
public partial class transfer_register
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]

File diff suppressed because it is too large Load Diff

View File

@@ -52,7 +52,6 @@
<EntityTypeShape EntityType="Model.stock_reason" Width="1.5" PointX="14.25" PointY="33.625" IsExpanded="true" />
<EntityTypeShape EntityType="Model.supplier" Width="1.5" PointX="14.25" PointY="36.5" IsExpanded="true" />
<EntityTypeShape EntityType="Model.supplier_kind" Width="1.5" PointX="12" PointY="37.5" IsExpanded="true" />
<EntityTypeShape EntityType="Model.sysdiagram" Width="1.5" PointX="0.75" PointY="6" IsExpanded="true" />
<AssociationConnector Association="Model.FK_accounting_accounting_kind" ManuallyRouted="false" />
<AssociationConnector Association="Model.FK_accounting_accounting_kind2" ManuallyRouted="false" />
<AssociationConnector Association="Model.FK_accounting_activity" ManuallyRouted="false" />
@@ -131,26 +130,38 @@
<AssociationConnector Association="Model.FK_transfer_register_actItem" />
<AssociationConnector Association="Model.FK_transfer_register_pro_order_detail" />
<AssociationConnector Association="Model.FK_pro_order_record_transfer_register" />
<EntityTypeShape EntityType="Model.GuaDanOrder" Width="1.5" PointX="13.375" PointY="21.25" />
<EntityTypeShape EntityType="Model.GuaDanOrderGuest" Width="2.125" PointX="30.375" PointY="13.75" />
<EntityTypeShape EntityType="Model.GuadanTimeSetting" Width="1.5" PointX="21.375" PointY="9.125" />
<EntityTypeShape EntityType="Model.Region" Width="1.5" PointX="23.625" PointY="3.875" />
<EntityTypeShape EntityType="Model.RegionAndRoomAndBedSchedule" Width="1.5" PointX="23.375" PointY="9.125" />
<EntityTypeShape EntityType="Model.RegionRoomBed" Width="2.125" PointX="28.125" PointY="4.625" />
<EntityTypeShape EntityType="Model.RegionRoomBedStatus" Width="1.5" PointX="25.875" PointY="0.75" />
<EntityTypeShape EntityType="Model.RegionType" Width="1.5" PointX="21.375" PointY="4.875" />
<EntityTypeShape EntityType="Model.Room" Width="1.5" PointX="25.875" PointY="4.375" />
<EntityTypeShape EntityType="Model.GuaDanOrder" Width="1.5" PointX="13.25" PointY="20.25" />
<EntityTypeShape EntityType="Model.GuaDanOrderGuest" Width="1.5" PointX="25.875" PointY="12.5" />
<EntityTypeShape EntityType="Model.GuadanTimeSetting" Width="1.5" PointX="21.375" PointY="7.75" />
<EntityTypeShape EntityType="Model.Region" Width="1.5" PointX="19.125" PointY="30.75" />
<EntityTypeShape EntityType="Model.RegionAndRoomAndBedSchedule" Width="1.5" PointX="28.125" PointY="12.625" />
<EntityTypeShape EntityType="Model.RegionRoomBed" Width="1.5" PointX="23.625" PointY="3.375" />
<EntityTypeShape EntityType="Model.RegionRoomBedStatus" Width="1.5" PointX="21.375" PointY="3.625" />
<EntityTypeShape EntityType="Model.RegionType" Width="1.5" PointX="16.875" PointY="31.625" />
<EntityTypeShape EntityType="Model.Room" Width="1.5" PointX="21.375" PointY="31.125" />
<AssociationConnector Association="Model.FK_GuaDanOrder_Admin_CreateUser" />
<AssociationConnector Association="Model.FK_GuaDanOrder_Followers" />
<AssociationConnector Association="Model.FK_GuaDanOrderGuest_FOLLOWERS" />
<AssociationConnector Association="Model.FK_GuaDanOrderGuest_BedUuid" />
<AssociationConnector Association="Model.FK_GuaDanOrderGuest_RoomUuid" />
<AssociationConnector Association="Model.FK_GuaDanOrderGuest_Status" />
<AssociationConnector Association="Model.FK_GuaDanOrderGuest_StatusCode" />
<AssociationConnector Association="Model.FK_Schedule_GuaDanOrderGuest" />
<AssociationConnector Association="Model.FK_Region_ParentUuid" />
<AssociationConnector Association="Model.FK_Region_RegionTypeUuid" />
<AssociationConnector Association="Model.FK_Room_Region" />
<AssociationConnector Association="Model.FK_RegionRoomBed_RegionRoomBedStatus" />
<AssociationConnector Association="Model.FK_RegionRoomBed_RoomUuid" />
<AssociationConnector Association="Model.FK_RegionRoomBed_StatusCode" />
<EntityTypeShape EntityType="Model.AncestralTabletArea" Width="1.5" PointX="25.375" PointY="22.375" />
<EntityTypeShape EntityType="Model.AncestralTabletPosition" Width="1.5" PointX="27.625" PointY="18.375" />
<EntityTypeShape EntityType="Model.AncestralTabletPositionRecord" Width="1.5" PointX="32.125" PointY="18.625" />
<EntityTypeShape EntityType="Model.AncestralTabletRegistrant" Width="1.5" PointX="29.875" PointY="18" />
<EntityTypeShape EntityType="Model.AncestralTabletStatus" Width="1.5" PointX="25.375" PointY="19.125" />
<AssociationConnector Association="Model.FK_AncestralTabletArea_Parent" />
<AssociationConnector Association="Model.FK_Position_Area" />
<AssociationConnector Association="Model.FK_Position_Status" />
<AssociationConnector Association="Model.FK_Registrant_Position" />
<AssociationConnector Association="Model.FK__Ancestral__Regis__5A1A5A11" />
<AssociationConnector Association="Model.FK_GuaDanOrderGuest_Order" />
</Diagram>
</edmx:Diagrams>
</edmx:Designer>

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
/// <summary>
/// GuaDanOrderGuest 的摘要描述
/// </summary>
namespace Model
{
public partial class GuaDanOrderGuest
{
// 状态常量定义
public const string STATUS_BOOKED = "401"; // 预订成功
public const string STATUS_CHECKED_IN = "402"; // 已入住
public const string STATUS_CHECKED_OUT = "403"; // 已退房
public const string STATUS_CANCELLED = "404"; // 已取消
public static bool IsStatusTransitionValid(ezEntities db, string targetStatus, Guid guestId)
{
// 获取当前客人对象
GuaDanOrderGuest currentGuest = db.GuaDanOrderGuest.Find(guestId);
if (currentGuest == null)
{
// 如果没有客人对象,只能创建新的预订
return targetStatus == STATUS_BOOKED;
}
// 安全获取当前状态处理null情况
string currentStatus = currentGuest.RegionRoomBedStatus?.Code;
// 如果当前状态为空,只能进入预订成功状态
if (string.IsNullOrEmpty(currentStatus))
{
return targetStatus == STATUS_BOOKED;
}
// 定义有效的状态转换规则
var validTransitions = new Dictionary<string, List<string>>
{
{ STATUS_BOOKED, new List<string> { STATUS_CHECKED_IN, STATUS_CANCELLED } },
{ STATUS_CHECKED_IN, new List<string> { STATUS_CHECKED_OUT } },
{ STATUS_CHECKED_OUT, new List<string> { } }, // 终态,不能再转换
{ STATUS_CANCELLED, new List<string> { } } // 终态,不能再转换
};
// 检查转换是否有效
if (validTransitions.ContainsKey(currentStatus))
{
return validTransitions[currentStatus].Contains(targetStatus);
}
return false; // 未知的当前状态
}
}
}

View File

@@ -58,7 +58,7 @@ namespace Model
// 找出在日期範圍內被占用的床位 Uuid包括長期占用 ScheduleDate = null
var busyBedUuidsQuery = db.RegionAndRoomAndBedSchedule
.Where(s => s.IsDeleted == false && s.IsActive
.Where(s => s.IsDeleted == false && !s.IsCancel
&& (s.ScheduleDate == null
|| (end.HasValue
&& s.ScheduleDate >= start
@@ -79,25 +79,28 @@ namespace Model
public static async Task<bool> IsBedAvailableAsync(ezEntities db, Guid targetUuid, DateTime start, DateTime? end)
{
// 如果 end 為 null表示長期占用直接判斷是否已有長期占用
//不包含结束时间那一天
if (end == null)
{
var hasLongTerm = await db.RegionAndRoomAndBedSchedule
.AnyAsync(s => s.IsDeleted == false
&& s.IsActive
&& !s.IsCancel
&& s.TargetUuid == targetUuid
&& s.ScheduleDate == null);
return !hasLongTerm;
}
// 短期占用,查詢每日排程中有無衝突
var totalDays = (end.Value.Date - start.Date).Days + 1;
var totalDays = (end.Value.Date - start.Date).Days;
for (int i = 0; i < totalDays; i++)
{
var date = start.Date.AddDays(i);
var conflict = await db.RegionAndRoomAndBedSchedule
.Where(s => s.GuaDanOrderGuest.StatusCode != GuaDanOrderGuest.STATUS_CANCELLED)
.Where(s => s.GuaDanOrderGuest.StatusCode != GuaDanOrderGuest.STATUS_CHECKED_OUT)
.AnyAsync(s => s.IsDeleted == false
&& s.IsActive
&& !s.IsCancel
&& s.TargetUuid == targetUuid
&& s.ScheduleDate == date);
@@ -114,7 +117,10 @@ namespace Model
// 找出所有在日期範圍內被占用的床位
var busyBedUuids = await db.RegionAndRoomAndBedSchedule
.Where(s => s.IsDeleted == false && s.IsActive
.Where(s => s.GuaDanOrderGuest.StatusCode != "403")
.Where(s => s.GuaDanOrderGuest.StatusCode != "404")
.Where(a => a.IsCancel == false)
.Where(s => s.IsDeleted == false
&& (s.ScheduleDate == null // 長期占用
|| (s.ScheduleDate >= start && s.ScheduleDate <= end)))
.Select(s => s.TargetUuid)

View File

@@ -11,6 +11,9 @@ namespace Model
{
public partial class RegionRoomBed
{
public const string STATUS_BED_FREE = "101"; // 空閒,可使用
public const string STATUS_BED_OCCUPIED = "102"; // 已佔用
public const string STATUS_BED_MAINTENANCE = "103"; // 維護中,不可使用
public bool IsAvailable()
{
//判断床位是否可用:自身是否启用
@@ -34,7 +37,9 @@ namespace Model
// 如果資料庫 ScheduleDate 是 date 型別,本身沒有時間部分,可以直接比較
var conflict = _db.RegionAndRoomAndBedSchedule.Any(s =>
s.TargetUuid == this.Uuid &&
s.IsActive &&
s.GuaDanOrderGuest.StatusCode != "403" &&
s.GuaDanOrderGuest.StatusCode != "404" &&
!s.IsCancel &&
!s.IsDeleted &&
(
s.ScheduleDate == null || // 長期占用
@@ -46,5 +51,6 @@ namespace Model
return !conflict;
}
}
}

View File

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations;
/// <summary>
/// added
/// </summary>
namespace Model
{
[MetadataType(typeof(family_membersMetadata))]
public partial class family_members
{
private class family_membersMetadata
{
[JsonIgnore]
public virtual follower follower { get; set; }
}
}
}

View File

@@ -45,6 +45,19 @@ namespace Model
public virtual ICollection<followers_tablet> followers_tablet { get; set; }
[JsonIgnore]
public virtual appellation appellation { get; set; }
/// <summary>
/// added
/// </summary>
[JsonIgnore]
public virtual ICollection<family_members> family_members { get; set; }
[JsonIgnore]
public virtual ICollection<transfer_register> transfer_register { get; set; }
[JsonIgnore]
public virtual ICollection<transfer_register> transfer_register1 { get; set; }
[JsonIgnore]
public virtual ICollection<GuaDanOrder> GuaDanOrder { get; set; }
[JsonIgnore]
public virtual ICollection<GuaDanOrderGuest> GuaDanOrderGuest { get; set; }
}

View File

@@ -19,5 +19,6 @@ public class GuaDanOrderView
public string bookerName { get; set; }
public string bookerPhone { get; set; }
public int? bookerFollowerNum { get; set; }
public int? activityNum { get; set; }
}

View File

@@ -1,73 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data;
using System.Configuration;
using System.Collections;
using Newtonsoft.Json;
using System.ComponentModel;
using System.Globalization;
using System.Web.UI;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.IO.Compression;
using System.Net.Mail;
using System.Configuration;
using System.Drawing;
using System.Drawing.Imaging;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data.OleDb;
using Microsoft.VisualBasic;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.Security;
using System.Security.Cryptography;
using System.Web.UI;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.Design;
using System.ComponentModel.Design.Serialization;
using System.Configuration;
using System.Data;
using System.Data.OleDb;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Reflection;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Caching;
using System.Web.ModelBinding;
using System.Web.Routing;
using System.Web.Security;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.Adapters;
using System.Web.UI.HtmlControls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Collections;
using System.Data;
using System.Data.OleDb;
using System.Configuration;
using System.Web.UI;
using System.Web.UI.WebControls;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Web.UI;
using System.Text;
using System.Text.RegularExpressions;
using System.Net;
using System.Reflection;
using System.ComponentModel;
using System.Web.UI.WebControls;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace Model.ViewModel

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
namespace Model
{
public static class StatusTransitionManager
{
private static readonly Dictionary<string, List<string>> transitions =
new Dictionary<string, List<string>>
{
// 掛單狀態
{ "401", new List<string> { "402", "404" } },
{ "402", new List<string> { "403" } },
{ "403", new List<string>() },
{ "404", new List<string>() },
// 床位狀態
{ "101", new List<string> {"101", "102","103"} },
{ "102", new List<string> { "101" } },
{ "103", new List<string> { "101" } },
};
public static bool CanTransition(string currentCode, string targetCode)
{
if (string.IsNullOrEmpty(currentCode))
{
return targetCode == "401" || targetCode == "402" || targetCode == "101";
}
if(string.IsNullOrEmpty(targetCode))
{ return false; }
return transitions.ContainsKey(currentCode) &&
transitions[currentCode].Contains(targetCode);
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
/// <summary>
/// ActivityStatisticsController 的摘要描述
/// </summary>
public class ActivityStatisticsController: ApiController
{
private Model.ezEntities _db = new Model.ezEntities();
[HttpGet]
[Route("api/activity/statistics/summary")]
public IHttpActionResult GetStatisticsSummary([FromUri] int? activity_num =null)
{
if (activity_num == null)
{
return BadRequest("活动Id不能为空");
}
var now = DateTime.Now;
var pre_order_query = _db.pro_order.Where(a => a.activity_num == activity_num);
var totalApplicants = pre_order_query.Count();
var maleApplicants = pre_order_query.Where(a => a.follower.sex == "男眾").Count();
var pro_order_detail_query = _db.pro_order_detail
.Where(d => d.pro_order.activity_num == activity_num);
var result = new
{
reportDate = now.ToString("yyyy/MM/dd HH:mm:ss"),
totalApplicants = totalApplicants, //报名总人数
maleApplicants = maleApplicants, //男
femaleApplicants = totalApplicants - maleApplicants,//女
donation = new
{
total = 158000,//总功德金
received = 150000,//已收功德金
unreceived = 8000//未收功德金
},
items = new[]//功德项目,这个的功德项目要根据活动预设的功德项目来做统计,就是这个活动有那些可报名的功德项目
{
new { name = "總功德主", count = "10人" },
new { name = "利益主", count = "15人" },
new { name = "個人大牌", count = "50人" },
new { name = "供僧", count = "45人" }
},
plaques = new[]//牌位
{
new { name = "總牌位數", count = pro_order_detail_query.Count() },
new { name = "消災-大牌位", count = 101 },
new { name = "超冤-大牌位", count =10 },
new { name = "消災-個人大牌", count =10 },
new { name = "超冤-個人大牌", count = 10 },
new { name = "超薦-個人大牌", count = 10 },
new { name = "消災-個人中牌", count = 10 },
new { name = "超冤-個人中牌", count = 10 },
new { name = "超薦-個人中牌", count = 10 },
new { name = "消災-隨喜牌位", count = 10 },
new { name = "超冤-隨喜牌位", count = 10 },
new { name = "超薦-隨喜牌位", count = 10 },
new { name = "消災-常年牌位", count = 10 },
new { name = "超冤-常年牌位", count = 10 },
new { name = "超薦-常年牌位", count = 10 },
new { name = "消災-急立牌位", count = 10 },
new { name = "超薦-急立牌位", count = 10 },
new { name = "超冤-急立牌位", count = 101 },
}
};
return Ok(result);
}
}

View File

@@ -0,0 +1,180 @@
using Model;
using PagedList;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using static AncestralTabletPositionController;
using static regionController;
/// <summary>
/// AncestralTabletController 的摘要描述
/// </summary>
public class AncestralTabletAreaController: ApiController
{
private Model.ezEntities _db = new Model.ezEntities();
public class AreaViewModel
{
public int? AreaId { get; set; }
public string AreaName { get; set; }
[Required(ErrorMessage = "區域編號為必填")]
public string AreaCode { get; set; }
public int? ParentAreaId { get; set; }
public string AreaType { get; set; }
public int? Price { get; set; }
public int? SortOrder { get; set; }
public bool IsDisabled { get; set; } = false;
public string Description { get; set; }
}
public class RegionDto
{
public int AreaId { get; set; }
public string AreaName { get; set; }
public string AreaCode { get; set; }
public int? SortOrder { get; set; }
public int? ParentAreaId { get; set; }
public string AreaType { get; set; }
public bool IsDisabled { get; set; }
public string Description { get; set; }
public int? Price { set; get; }
public List<RegionDto> Children { get; set; } = new List<RegionDto>();
}
[HttpGet]
[Route("api/ancestraltablet/area/getlist")]
public IHttpActionResult GetList()
{
var allArea = _db.AncestralTabletArea.ToList();
var rootRegions = allArea
.Where(r => r.ParentAreaId == null)
.OrderBy(r => r.SortOrder)
.ToList();
var tree = rootRegions
.Select(r => BuildRegionDto(r, allArea))
.ToList();
return Ok(tree);
}
[HttpGet]
[Route("api/ancestraltablet/area/getereawithposition")]
public IHttpActionResult GetEreaWithPosition()
{
//获取有神位位置的区域
var allArea = _db.AncestralTabletArea
.Where(a => a.AncestralTabletPosition.Count()>0)
.Select(a => new
{
a.AreaId,
a.AreaName,
})
.ToList();
return Ok(allArea);
}
private RegionDto BuildRegionDto(AncestralTabletArea area, List<AncestralTabletArea> allArea)
{
return new RegionDto
{
AreaId = area.AreaId,
AreaName = area.AreaName,
AreaCode = area.AreaCode,
SortOrder = area.SortOrder,
ParentAreaId = area.ParentAreaId,
AreaType = area.AreaType,
IsDisabled = area.IsDisabled,
Description = area.Description,
Price = area.Price,
Children = allArea
.Where(r => r.ParentAreaId == area.AreaId)
.OrderBy(r => r.SortOrder)
.Select(child => BuildRegionDto(child, allArea))
.ToList(),
};
}
[HttpPost]
[Route("api/ancestraltablet/area/create")]
public async Task<IHttpActionResult> CreateArea([FromBody] AreaViewModel tabletArea)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
try
{
var area = new AncestralTabletArea
{
AreaName = tabletArea.AreaName,
AreaCode = tabletArea.AreaCode,
ParentAreaId = tabletArea.ParentAreaId,
AreaType = tabletArea.AreaType,
Price = tabletArea.Price,
SortOrder = tabletArea.SortOrder,
IsDisabled = tabletArea.IsDisabled,
Description = tabletArea.Description
};
_db.AncestralTabletArea.Add(area);
await _db.SaveChangesAsync();
tabletArea.AreaId = area.AreaId;
return Ok(new { message = "區域建立成功", area = tabletArea });
}
catch (Exception ex)
{
return InternalServerError(ex);
}
}
[HttpPost]
[Route("api/ancestraltablet/area/edit")]
public async Task<IHttpActionResult> EditArea([FromBody] AreaViewModel tabletArea)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
try
{
var oldArea = _db.AncestralTabletArea.Find(tabletArea.AreaId);
if (oldArea == null)
{
return NotFound();
}
oldArea.AreaName = tabletArea.AreaName;
oldArea.AreaCode = tabletArea.AreaCode;
oldArea.ParentAreaId = tabletArea.ParentAreaId;
oldArea.Price = tabletArea.Price;
oldArea.SortOrder = tabletArea.SortOrder;
oldArea.Description = tabletArea.Description;
oldArea.IsDisabled = tabletArea.IsDisabled;
await _db.SaveChangesAsync();
return Ok(new { message = "區域修改成功", area = tabletArea });
}
catch (Exception ex)
{
return InternalServerError(ex);
}
}
[HttpGet]
[Route("api/ancestraltablet/area/position/getlist")]
public IHttpActionResult GetPositionList([FromUri] int areaId)
{
var positions = _db.AncestralTabletPosition
.Where(p => p.AreaId == areaId)
.Select(p => new AncestralTabletPositionDto
{
PositionId = p.PositionId,
AreaId = p.AreaId,
PositionCode = p.PositionCode,
PositionName = p.PositionName,
Price = p.Price,
StatusCode = p.StatusCode,
Description = p.Description,
RowNo = p.RowNo,
ColumnNo = p.ColumnNo
})
.ToList();
return Ok(positions);
}
}

View File

@@ -0,0 +1,184 @@
using Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Http;
/// <summary>
/// AncestralTabletPositionController 的摘要描述
/// </summary>
public class AncestralTabletPositionController: ApiController
{
private Model.ezEntities _db = new Model.ezEntities();
[HttpGet]
[Route("api/ancestraltablet/position/getlist")]
public IHttpActionResult GetList([FromUri] int areaId)
{
var positions = _db.AncestralTabletPosition
.Where(p => p.AreaId == areaId)
.Select(p => new
{
PositionId = p.PositionId,
AreaId = p.AreaId,
PositionCode = p.PositionCode,
PositionName = p.PositionName,
Price = p.Price,
StatusCode = p.StatusCode,
Description = p.Description,
RowNo = p.RowNo,
ColumnNo = p.ColumnNo,
AncestralTabletRegistrant = _db.AncestralTabletRegistrant
.Where(r => r.PositionId == p.PositionId && r.IsActive == true)
.Select(r => new
{
r.RegistrantCode,
r.Name,
r.Phone,
r.Address,
r.RegisterDate,
r.Price,
r.StartDate,
r.EndDate,
r.IsLongTerm,
r.IsActive,
// 嵌套查询牌位记录PositionRecord
TabletRecord = r.AncestralTabletPositionRecord
.Select(pr => new
{
pr.RecordId,
pr.RegistrantCode,
pr.NPTitle,
pr.NPStandDate,
pr.NPYangShang,
pr.WPContent
})
.FirstOrDefault()
})
.FirstOrDefault()
})
.ToList();
return Ok(positions);
}
[HttpGet]
[Route("api/ancestraltablet/position/shortlist")]
public IHttpActionResult GetListWithShort([FromUri] int areaId)
{
//获取位置列表,简单信息
var positions = _db.AncestralTabletPosition
.Where(p => p.AreaId == areaId)
.Select(p => new
{
PositionId = p.PositionId,
AreaId = p.AreaId,
PositionCode = p.PositionCode,
PositionName = p.PositionName,
Price = p.Price,
StatusCode = p.StatusCode,
Description = p.Description,
RowNo = p.RowNo,
ColumnNo = p.ColumnNo,
isCanUse = p.StatusCode == "available" ? true : false,
})
.ToList();
return Ok(positions);
}
[HttpPost]
[Route("api/ancestraltablet/position/batchcreate")]
public IHttpActionResult BatchCreatePosition([FromBody] List<AncestralTabletPositionDto> positions)
{
if (positions == null || positions.Count == 0)
return BadRequest("未接收到任何位置数据");
try
{
foreach (var dto in positions)
{
var entity = new AncestralTabletPosition
{
AreaId = dto.AreaId,
PositionCode = dto.PositionCode,
PositionName = dto.PositionName,
Price = dto.Price,
StatusCode = dto.StatusCode,
Description = dto.Description,
RowNo = dto.RowNo,
ColumnNo = dto.ColumnNo
};
_db.AncestralTabletPosition.Add(entity);
}
_db.SaveChanges();
return Ok(new { message = "批量新增成功", count = positions.Count });
}
catch (Exception ex)
{
string message = ex.InnerException?.InnerException?.Message ?? ex.Message;
return Content(HttpStatusCode.InternalServerError, new
{
message = "批量新增失败",
exceptionMessage = message
});
}
}
[HttpPost]
[Route("api/ancestraltablet/position/edit")]
public IHttpActionResult EditPosition([FromBody] AncestralTabletPositionDto pos)
{
var oldPos = _db.AncestralTabletPosition
.FirstOrDefault(p => p.AreaId == pos.AreaId && p.PositionCode == pos.PositionCode);
if(oldPos == null) return NotFound();
try
{
oldPos.PositionName = pos.PositionName;
oldPos.Price = pos.Price;
oldPos.StatusCode = pos.StatusCode;
// 保存到数据库
_db.SaveChanges();
}
catch (Exception ex)
{
return InternalServerError(ex);
}
return Ok(new { message="更新成功", code=200});
}
[HttpDelete]
[Route("api/ancestraltablet/position/delete/{positionId}")]
public IHttpActionResult DeletePosition(int positionId)
{
var pos = _db.AncestralTabletPosition.FirstOrDefault(p => p.PositionId == positionId);
if (pos == null)
{
return NotFound();
}
_db.AncestralTabletPosition.Remove(pos);
_db.SaveChanges();
return Ok("删除成功");
}
public class AncestralTabletPositionDto
{
public int PositionId { get; set; }
public int AreaId { get; set; }
public string PositionCode { get; set; }
public string PositionName { get; set; }
public int? Price { get; set; }
public string StatusCode { get; set; }
public string Description { get; set; }
public int? RowNo { get; set; }
public int? ColumnNo { get; set; }
}
}

View File

@@ -0,0 +1,361 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.UI.WebControls;
/// <summary>
/// AncestralTabletRecordController 的摘要描述
/// </summary>
public class AncestralTabletRecordController: ApiController
{
private Model.ezEntities _db = new Model.ezEntities();
[HttpGet]
[Route("api/ancestraltablet/registrant/getlist")]
public IHttpActionResult GetRegistrantList()
{
//获取登记人列表
var data = _db.AncestralTabletRegistrant.Select(x => new
{
x.RegistrantCode,
x.Name,
x.Phone,
x.Address,
x.RegisterDate,
x.Price,
x.PositionId,
x.StartDate,
x.EndDate,
x.IsLongTerm,
x.IsActive,
x.CreatedAt,
x.UpdatedAt
})
.Take(1000) // 取前1000条
.ToList(); ;
return Ok(data);
}
[HttpPost]
[Route("api/ancestraltablet/registrant/getlistbypage")]
public IHttpActionResult GetRegistrantListByPage([FromBody] RegistrantSearchDto searchDto)
{
//获取登记人列表
var query = _db.AncestralTabletRegistrant.AsQueryable();
if( !string.IsNullOrEmpty(searchDto.searchName))
{
query = query.Where(r => r.Name == searchDto.searchName);
}
var tatol = query.Count();
var data = query.Select(x => new
{
x.RegistrantCode,
x.Name,
x.Phone,
x.Address,
x.RegisterDate,
x.Price,
x.PositionId,
x.StartDate,
x.EndDate,
x.IsLongTerm,
x.IsActive,
x.CreatedAt,
x.UpdatedAt
})
.OrderByDescending(a => a.CreatedAt)
.Skip((searchDto.page - 1) * searchDto.pageSize)
.Take(searchDto.pageSize) // 取前1000条
.ToList();
return Ok(new
{
data = data,
total = tatol
});
}
[HttpGet]
[Route("api/ancestraltablet/registrant/getbycode")]
public IHttpActionResult GetRegistrantByCode([FromUri] string registrantCode)
{
var r = _db.AncestralTabletRegistrant.Find(registrantCode);
if (r == null)
{
return NotFound();
}
var rDto = new
{
r.RegistrantCode,
r.Name,
r.Phone,
r.Address,
r.RegisterDate,
r.Price,
r.StartDate,
r.EndDate,
r.IsLongTerm,
r.IsActive,
r.PositionId,
positionName = r.AncestralTabletPosition?.PositionName,
// 嵌套查询牌位记录PositionRecord
TabletRecord = r.AncestralTabletPositionRecord
.Select(pr => new
{
pr.RecordId,
pr.RegistrantCode,
pr.NPTitle,
pr.NPStandDate,
pr.NPYangShang,
pr.WPContent,
})
.FirstOrDefault()
};
return Ok(rDto);
}
[HttpPost]
[Route("api/ancestraltablet/registrant/create")]
public IHttpActionResult CreateRegistrant([FromBody] AncestralTabletRegistrantDto dto)
{
//新增登记人api
if (dto == null)
{
return BadRequest("请求体不能为空");
}
if(!string.IsNullOrEmpty(dto.RegistrantCode))
{
return BadRequest("RegistrantCode 应传递空");
}
try
{
dto.RegistrantCode = GenerateRegistrantCode();
// 设置默认创建时间
dto.CreatedAt = DateTime.Now;
var entity = new Model.AncestralTabletRegistrant
{
RegistrantCode = dto.RegistrantCode,
Name = dto.Name,
Phone = dto.Phone,
Address = dto.Address,
RegisterDate = dto.RegisterDate,
Price = dto.Price,
PositionId = dto.PositionId,
StartDate = dto.startDate,
EndDate = dto.endDate,
IsLongTerm = dto.isLongTerm,
IsActive = dto.isActive,
CreatedAt = dto.CreatedAt,
UpdatedAt = dto.UpdatedAt
};
// 假设你有一个 EF DbContext如 _db
_db.AncestralTabletRegistrant.Add(entity);
if (dto.PositionId != null)
{
var position = _db.AncestralTabletPosition
.FirstOrDefault(p => p.PositionId == dto.PositionId);
if (position != null)
{
position.StatusCode = "used"; // 或者根据你的枚举/字段设置
}
}
_db.SaveChanges();
return Ok(new { message = "登记成功", registrantCode = entity.RegistrantCode });
}
catch (Exception ex)
{
return InternalServerError(ex);
}
}
[HttpPost]
[Route("api/ancestraltablet/registrant/update")]
public IHttpActionResult UpdateRegistrant([FromBody] AncestralTabletRegistrantDto dto)
{
if (dto == null)
{
return BadRequest("请求体不能为空");
}
if (string.IsNullOrWhiteSpace(dto.RegistrantCode))
{
return BadRequest("缺少 RegistrantCode无法进行更新操作");
}
try
{
// 查找原始资料
var entity = _db.AncestralTabletRegistrant
.FirstOrDefault(r => r.RegistrantCode == dto.RegistrantCode);
if (entity == null)
{
return NotFound(); // 没有对应记录
}
// ===== 处理安位状态 =====
if (entity.PositionId != dto.PositionId)
{
// 1. 原来的安位设置为可用
if (entity.PositionId != null)
{
var oldPosition = _db.AncestralTabletPosition
.FirstOrDefault(p => p.PositionId == entity.PositionId);
if (oldPosition != null)
{
oldPosition.StatusCode = "available"; // 可用
}
}
// 2. 新的安位设置为已使用
if (dto.PositionId != null)
{
var newPosition = _db.AncestralTabletPosition
.FirstOrDefault(p => p.PositionId == dto.PositionId);
if (newPosition != null)
{
newPosition.StatusCode = "used"; // 已使用
}
}
// 更新登记人安位
entity.PositionId = dto.PositionId;
}
// 更新其它字段
entity.Name = dto.Name;
entity.Phone = dto.Phone;
entity.Address = dto.Address;
entity.RegisterDate = dto.RegisterDate;
entity.Price = dto.Price;
entity.StartDate = dto.startDate;
entity.EndDate = dto.endDate;
entity.IsLongTerm = dto.isLongTerm;
entity.IsActive = dto.isActive;
entity.UpdatedAt = DateTime.Now;
_db.SaveChanges();
return Ok(new { message = "登记人更新成功", registrantCode = entity.RegistrantCode });
}
catch (Exception ex)
{
return InternalServerError(ex);
}
}
[HttpPost]
[Route("api/ancestraltablet/pw/create")]
public IHttpActionResult CreatePW([FromBody] AncestralTabletPositionRecordDto dto)
{
if (dto == null)
return BadRequest("请求体不能为空");
if (string.IsNullOrEmpty(dto.RegistrantCode))
return BadRequest("登记人编号RegistrantCode不能为空");
try
{
// 映射到数据库实体
var entity = new Model.AncestralTabletPositionRecord
{
RegistrantCode = dto.RegistrantCode,
NPTitle = dto.NPTitle,
NPStandDate = dto.NPStandDate,
NPYangShang = dto.NPYangShang,
WPContent = dto.WPContent,
CreatedAt = DateTime.Now,
UpdatedAt = null
};
_db.AncestralTabletPositionRecord.Add(entity);
_db.SaveChanges();
return Ok(new { message = "牌位登记成功", recordId = entity.RecordId });
}
catch (Exception ex)
{
return InternalServerError(ex);
}
}
[HttpPost]
[Route("api/ancestraltablet/pw/update")]
public IHttpActionResult UpdatePW([FromBody] AncestralTabletPositionRecordDto dto)
{
if (dto == null)
return BadRequest("请求体不能为空");
try
{
var entity = _db.AncestralTabletPositionRecord.Find(dto.RecordId);
// 映射到数据库实体
if (entity == null)
return BadRequest("牌位不存在,更新失败");
entity.RegistrantCode = dto.RegistrantCode;
entity.NPTitle = dto.NPTitle;
entity.NPStandDate = dto.NPStandDate;
entity.NPYangShang = dto.NPYangShang;
entity.WPContent = dto.WPContent;
entity.UpdatedAt = DateTime.Now;
_db.SaveChanges();
return Ok(new { message = "牌位更新成功", recordId = entity.RecordId });
}
catch (Exception ex)
{
return InternalServerError(ex);
}
}
public string GenerateRegistrantCode(string prefix = "REG", int randomLength = 6)
{
string datePart = DateTime.Now.ToString("yyyyMMdd");
string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
var suffix = new string(Enumerable.Repeat(chars, randomLength)
.Select(s => s[random.Next(s.Length)]).ToArray());
return $"{prefix}{datePart}{suffix}";
}
public class AncestralTabletRegistrantDto
{
public string RegistrantCode { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
public string Address { get; set; }
public DateTime RegisterDate { get; set; }
public int? Price { get; set; }
public int? PositionId { get; set; }
public DateTime startDate { get; set; }
public DateTime? endDate { get; set; }
public bool isLongTerm { get; set; } = false;
public bool isActive { get; set; } = true;
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
public class AncestralTabletPositionRecordDto
{
public int? RecordId { get; set; }
public string RegistrantCode { get; set; }
public string NPTitle { get; set; }
public DateTime NPStandDate { get; set; }
public string NPYangShang { get; set; }
public string WPContent { get; set; }
}
public class RegistrantSearchDto
{
public int page { get; set; } = 1;
public int pageSize { get; set; } = 10;
public string searchName { get; set; }
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
/// <summary>
/// AncestralTabletStatisticsController 的摘要描述
/// </summary>
public class AncestralTabletStatisticsController:ApiController
{
private Model.ezEntities db = new Model.ezEntities();
[HttpGet]
[Route("api/ancestraltablet/statistics/positions/availablepositions")]
public IHttpActionResult GetAvailablePositions()
{
var query =
from a in db.AncestralTabletArea // 区域表
join p in db.AncestralTabletPosition
on a.AreaId equals p.AreaId into ap
from p in ap.DefaultIfEmpty()
join r in db.AncestralTabletRegistrant
on p.PositionId equals r.PositionId into pr
from r in pr.DefaultIfEmpty()
group new { a, p, r } by new { a.AreaId, a.AreaName } into g
select new
{
AreaId = g.Key.AreaId,
AreaName = g.Key.AreaName,
TotalPositions = g.Count(x => x.p != null), // 总位置数
AvailableCount = g.Count(x => x.p != null && x.r == null) // 可用位置数(未登记)
};
var result = query.ToList();
return Ok(result);
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
/// <summary>
/// AncestralTabletStatusController 的摘要描述
/// </summary>
public class AncestralTabletStatusController:ApiController
{
private Model.ezEntities _db = new Model.ezEntities();
[HttpGet]
[Route("api/ancestraltablet/status/list")]
public IHttpActionResult GetStatusList()
{
var statusList = _db.AncestralTabletStatus
.Select(s => new
{
s.StatusCode,
s.StatusName,
s.StatusType
})
.ToList();
return Ok(statusList);
}
}

View File

@@ -0,0 +1,184 @@
using Model;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Http.Results;
using static GuaDanStatusCode;
/// <summary>
/// HandleBedInUsedController 的摘要描述
/// </summary>
public class HandleBedInUsedController : ApiController
{
private Model.ezEntities _db = new Model.ezEntities();
public HandleBedInUsedController()
{
//
// TODO: 在這裡新增建構函式邏輯
//
}
[HttpPost]
[Route("api/bed/inuse/list")]
public IHttpActionResult Get([FromBody] UuidModel uuidModel)
{
//獲取已被預約或者正在入住的床位,如果有指定就會查詢指定條件,如果沒有指定就會返回所有
var query = _db.GuaDanOrderGuest
.Where(gd => gd.StatusCode == GuaDanStatusCode.Guadan.CheckedIn
|| gd.StatusCode == GuaDanStatusCode.Guadan.Booked).ToList();
if (uuidModel.bedUuid.HasValue)
{
// 優先按床位查詢
query = query.Where(g => g.BedUuid == uuidModel.bedUuid.Value).ToList();
}
else if (uuidModel.roomUuid.HasValue)
{
// 如果沒有 bed但有 room
query = query.Where(g => g.RoomUuid == uuidModel.roomUuid.Value).ToList();
}
else if (uuidModel.regionUuid.HasValue)
{
// 如果只有 region
//query = query.Where(g => g.Room.RegionUuid == uuidModel.regionUuid.Value);
query = query.Where(g => IsRegionOrAncestor(g.Room, uuidModel.regionUuid.Value)).ToList();
}
var data = query.Select(g => new
{
g.BedUuid,
g.RoomUuid,
g.Room.RegionUuid,
g.GuaDanOrderNo,
g.RegionRoomBed.Name,
fullName = GetFullBedName(g.BedUuid.Value),
g.followers.u_name,
guadan_during = new { g.CheckInAt, g.CheckOutAt },
status = new { g.StatusCode, g.RegionRoomBedStatus.Name }
});
return Ok(data.ToList());
}
[HttpPost]
[Route("api/bed/inuse/cancel/singlebed/booking")]
public IHttpActionResult CancelSingleBedBooking([FromBody] UuidModel uuidModel)
{
if (uuidModel?.bedUuid == null)
return BadRequest("床位ID不能為空");
using (var transaction = _db.Database.BeginTransaction())
{
try
{
// 查詢符合條件的訂單
var orders = _db.GuaDanOrderGuest
.Where(g => g.BedUuid == uuidModel.bedUuid)
.Where(g => g.StatusCode == GuaDanStatusCode.Guadan.Booked || g.StatusCode == GuaDanStatusCode.Guadan.CheckedIn)
.ToList();
if (!orders.Any())
return NotFound();
// 更新狀態
foreach (var order in orders)
{
if (!StatusTransitionManager.CanTransition(order.StatusCode, GuaDanStatusCode.Guadan.Cancelled))
{
return BadRequest("當前狀態不能被取消");
}
order.StatusCode = GuaDanStatusCode.Guadan.Cancelled; // 假設Cancelled是取消狀態
}
var schedules = _db.RegionAndRoomAndBedSchedule
.Where(s => s.TargetUuid == uuidModel.bedUuid)
.Where(s => s.GuaDanOrderGuest.StatusCode == GuaDanStatusCode.Guadan.Booked
|| s.GuaDanOrderGuest.StatusCode == GuaDanStatusCode.Guadan.CheckedIn)
.ToList();
foreach (var schedule in schedules)
{
schedule.IsCancel = true;
}
_db.SaveChanges();
transaction.Commit();
return Ok(new { message = "取消成功", cancelledCount = orders.Count });
}
catch (Exception ex)
{
transaction.Rollback();
return InternalServerError(ex);
}
}
}
[HttpGet]
[Route("api/bed/inuse/region/list")]
public IHttpActionResult GetRegionList()
{
var regions = _db.Region
.Select(r => new
{
r.Uuid,
r.Name,
})
.ToList();
return Ok(regions);
}
[HttpGet]
[Route("api/bed/inuse/room/list")]
public IHttpActionResult GetRoomList([FromUri] Guid? regionUuid = null)
{
var room = _db.Room.Where(r => !r.IsDeleted && r.IsActive.Value).ToList();
if (regionUuid != null)
{
room = room.Where(r => IsRegionOrAncestor(r, regionUuid.Value)).ToList();
}
var data = room.Select(r => new
{
r.Uuid,
r.Name,
fullName = r.Region.Name + "/" + r.Name,
}).ToList();
return Ok(data);
}
public string GetFullBedName(Guid bedUuid)
{
var bed = _db.RegionRoomBed.Find(bedUuid);
if (bed == null)
return "";
var name = bed.Name;
var room = bed.Room;
if (room == null)
return name;
name = room.Name + "/" + name;
var region = room?.Region;
while (region != null)
{
name = region.Name + "/" + name;
region = region.Region2; // 遞迴向上
}
return name;
}
bool IsRegionOrAncestor(Model.Room room, Guid regionUuid)
{
//判斷傳入的regionuuid是否是room的祖先
if (room.RegionUuid == regionUuid)
return true;
var region = room.Region;
while (region != null)
{
if (region.Uuid == regionUuid) return true;
region = region.Region2;
}
return false;
}
public class UuidModel
{
public Guid? regionUuid = null;
public Guid? roomUuid = null;
public Guid? bedUuid = null;
}
}

View File

@@ -74,9 +74,11 @@ public class ShuWenController : ApiController
{
shuwen.ShuWenList = ProcessDesserts2(_db.pro_order_detail.Where(a => a.pro_order.activity_num == activitynum.Value).ToList());
}
catch
catch (Exception ex)
{
shuwen.IsGenerating = false;
_db.SaveChanges();
return BadRequest("生成舒文失败:" + ex.Message);
}
shuwen.IsGenerating = false;
shuwen.UpdateTime = DateTime.Now;
@@ -222,34 +224,42 @@ public class ShuWenController : ApiController
[Route("api/shuwen/download")]
public HttpResponseMessage DownloadShuWenWord(int? activitynum)
{
var data = _db.ShuWen.Where(a => a.ActivityNum == activitynum).FirstOrDefault();
if (data == null)
try
{
//return;
}
string json = data.ShuWenList;
string ActivityName = _db.activities.Where(a => a.num == data.ActivityNum).FirstOrDefault().subject;
if (json == null)
{
//return;
}
string fileName = $"疏文名單_{DateTime.Now:yyyyMMddHHmmss}.docx";
var stream = new MemoryStream();
GenerateShuWenWord_OpenXml(json, stream, ActivityName);
stream.Position = 0;
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StreamContent(stream)
};
response.Content.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.Content.Headers.ContentDisposition =
new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
var data = _db.ShuWen.Where(a => a.ActivityNum == activitynum).FirstOrDefault();
if (data == null)
{
FileName = fileName
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "活動編號不能為空");
}
string json = data.ShuWenList;
string ActivityName = _db.activities.Where(a => a.num == data.ActivityNum).FirstOrDefault().subject;
if (json == null)
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "疏文列表为空,无法生成 Word");
}
string fileName = $"疏文名單_{DateTime.Now:yyyyMMddHHmmss}.docx";
var stream = new MemoryStream();
GenerateShuWenWord_OpenXml(json, stream, ActivityName);
stream.Position = 0;
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StreamContent(stream)
};
return response;
response.Content.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.Content.Headers.ContentDisposition =
new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
{
FileName = fileName
};
return response;
}
catch (Exception ex)
{
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex.Message);
}
}
public void GenerateShuWenWord_OpenXml(string json, Stream outputStream, string ActivityName ="")

View File

@@ -98,7 +98,17 @@ public class familyMembersController : ApiController
[Route("api/familymembers/follower/{followerId}")]
public IHttpActionResult GetByFollower(int followerId)
{
//var originalLazyLoading = _db.Configuration.LazyLoadingEnabled;
//var originalProxyCreation = _db.Configuration.ProxyCreationEnabled;
//
//_db.Configuration.LazyLoadingEnabled = false;
//_db.Configuration.ProxyCreationEnabled = false;
var familyMembers = _db.family_members.Where(fm => fm.follower_num == followerId).ToList();
//_db.Configuration.LazyLoadingEnabled = originalLazyLoading;
//_db.Configuration.ProxyCreationEnabled = originalProxyCreation;
return Ok(familyMembers);
}

View File

@@ -0,0 +1,140 @@
using Model;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
/// <summary>
/// guadanGuestQueryController 的摘要描述
/// </summary>
public class guadanGuestQueryController: ApiController
{
private Model.ezEntities _db = new Model.ezEntities();
public guadanGuestQueryController()
{
//
// TODO: 在這裡新增建構函式邏輯
//
}
[HttpPost]
[Route("api/guadan/guest/query/list")]
public async Task<IHttpActionResult> GetList([FromBody] SearchGuestModel search)
{
var query = _db.GuaDanOrderGuest
.Where(guest => guest.StatusCode != "404");
if(search.SearchName != null)
{
query = query.Where(guest => guest.followers.u_name.Contains(search.SearchName));
}
if(search.searchCheckInDate != null)
{
query = query.Where(guest => guest.CheckInAt == search.searchCheckInDate);
}
if(search.searchCheckOutDate != null)
{
query = query.Where(guest => guest.CheckOutAt == search.searchCheckOutDate);
}
if(search.searchCheckInDateStart != null)
{
query = query.Where(guest => guest.CheckInAt >= search.searchCheckInDateStart);
}
if (search.searchCheckInDateEnd != null)
{
query = query.Where(guest => guest.CheckInAt <= search.searchCheckInDateEnd);
}
if (search.searchCheckOutDateStart != null)
{
query = query.Where(guest => guest.CheckOutAt >= search.searchCheckOutDateStart);
}
if (search.searchCheckOutDateEnd != null)
{
query = query.Where(guest => guest.CheckOutAt <= search.searchCheckOutDateEnd);
}
var totalCount = await query.CountAsync();
var pagedData = await query
.OrderByDescending(a => a.CheckInAt) // 可根据需要排序
.Skip((search.Page - 1) * search.PageSize)
.Take(search.PageSize)
.ToListAsync();
var data1 = pagedData.Select(a => new
{
name = a.followers != null ? a.followers.u_name : null,
checkindate = a.CheckInAt,
checkoutdate = a.CheckOutAt,
guadanorderno = a.GuaDanOrderNo,
roomName = GetRoomAndBedString(a.RegionRoomBed),
statusName = a.RegionRoomBedStatus.Name
}).ToList();
return Ok(new
{
items = data1,
total = totalCount,
});
}
[HttpGet]
[Route("api/guadan/guest/checkin/list")]
public async Task<IHttpActionResult> GetCheckInGuest([FromUri] DateTime date)
{
var today = DateTime.Now.Date;
var data = await _db.GuaDanOrderGuest
.Where(guest => guest.StatusCode == "402" || guest.StatusCode == "403")
.Where(guest => guest.RegionAndRoomAndBedSchedule
.Any(s => s.ScheduleDate == date.Date && s.ScheduleDate <= today) == true)
.Select(guest => new
{
name = guest.followers.u_name,
gender = guest.followers.sex,
})
.ToListAsync();
return Ok(data);
}
[HttpGet]
[Route("api/guadan/guest/booking/list")]
public async Task<IHttpActionResult> GetBookingGuest([FromUri] DateTime date)
{
var data = await _db.GuaDanOrderGuest
.Where(guest => guest.StatusCode == "402" || guest.StatusCode == "401" || guest.StatusCode == "403")
.Where(guest => guest.RegionAndRoomAndBedSchedule.Any(s => s.ScheduleDate == date.Date) == true)
.Select(guest => new
{
name = guest.followers.u_name,
gender = guest.followers.sex,
})
.ToListAsync();
return Ok(data);
}
public string GetRoomAndBedString(RegionRoomBed bed)
{
if (bed == null || bed.Room == null) return "";
var room = bed.Room;
var region = room.Region;
var name = room.Name + "/" + bed.Name;
if(region != null)
{
name = region.Name + "/" + name;
}
var parentRegion = region.Region2;
while (parentRegion != null)
{
name = parentRegion.Name + "/" + name;
parentRegion = parentRegion.Region2;
}
return name;
}
public class SearchGuestModel
{
public string SearchName = null;
public int Page = 1;
public int PageSize = 10;
public DateTime? searchCheckInDateStart = null;//入住日期的开始
public DateTime? searchCheckInDateEnd = null;//入住日期的结束
public DateTime? searchCheckOutDateStart = null;//退房日期的开始
public DateTime? searchCheckOutDateEnd = null;//退房日期的结束
public DateTime? searchCheckInDate = null;
public DateTime? searchCheckOutDate = null;
}
}

View File

@@ -1,4 +1,5 @@
using Model;
using PagedList;
using System;
using System.Collections.Generic;
using System.Data.Entity;
@@ -6,20 +7,54 @@ using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using static regionController;
/// <summary>
/// guadanOderController 的摘要描述
/// </summary>
[ezAuthorize]
public class guadanOrderController: ApiController
public class guadanOrderController : ApiController
{
private Model.ezEntities _db = new Model.ezEntities();
[HttpGet]
[HttpPost]
[Route("api/guadan/list")]
public async Task<IHttpActionResult> getGuadanList()
public async Task<IHttpActionResult> getGuadanList([FromBody] guadan_order_search_dto search)
{
var data = await _db.GuaDanOrder.OrderByDescending(b => b.CreatedAt)
var lastCheckoutTime = _db.GuadanTimeSetting.FirstOrDefault();
string lastCheckoutTimeStr = null;
if (lastCheckoutTime != null)
{
lastCheckoutTimeStr = lastCheckoutTime.LatestCheckOut;
}
var query = _db.GuaDanOrder
.Where(a => a.IsCancel == false)
.Where(a => a.IsDeleted == false);
if(!string.IsNullOrEmpty(search.guaDanOrderNo))
{
query = query.Where(order => order.GuaDanOrderNo == search.guaDanOrderNo);
}
if (search.guadanUser != null)
{
query = query.Where(order => order.BookerName == search.guadanUser);
}
if (search.startDate != null && search.endDate != null)
{
query = query.Where(order => order.StartDate >= search.startDate)
.Where(order => order.EndDate <= search.endDate);
}
else
{
if (search.startDate != null)
{
query = query.Where(order => order.StartDate == search.startDate);
}
else if (search.endDate != null)
{
query = query.Where(order => order.EndDate == search.endDate);
}
}
var total = query.Count();
var data1 = await query.ToListAsync();
var data = data1
.OrderByDescending(b => b.CreatedAt)
.Select(a => new
{
uuid = a.Uuid,
@@ -29,24 +64,65 @@ public class guadanOrderController: ApiController
created_at = a.CreatedAt,
updated_at = a.UpdatedAt,
notes = a.Notes,
is_timeout = !string.IsNullOrEmpty(lastCheckoutTimeStr) &&
_db.GuaDanOrderGuest
.Where(g => g.GuaDanOrderNo == a.GuaDanOrderNo && !g.IsDeleted && g.StatusCode == GuaDanOrderGuest.STATUS_CHECKED_IN)
.ToList()
.Any(g =>
g.CheckOutAt.HasValue &&
DateTime.Parse(g.CheckOutAt.Value.ToString("yyyy-MM-dd") + " " + lastCheckoutTimeStr) < DateTime.Now
),
activity = _db.activities
.Where(act => act.num == a.ActivityNum)
.Select(act => new
{
subject = act.subject
})
.FirstOrDefault(),
bookerName = a.BookerName,
guest_count = _db.GuaDanOrderGuest.Where(c => c.GuaDanOrderNo == a.GuaDanOrderNo).Count(),
}).ToListAsync();
return Ok(data);
guest_count = _db.GuaDanOrderGuest
.Where(c => c.GuaDanOrderNo == a.GuaDanOrderNo && c.IsDeleted == false)
.Where(c => c.RegionRoomBedStatus.Code != GuaDanOrderGuest.STATUS_CANCELLED)
.Count(),
guadan_status = _db.GuaDanOrderGuest
.Where(g => g.GuaDanOrderNo == a.GuaDanOrderNo && a.IsDeleted == false)
.Where(g => g.StatusCode != GuaDanOrderGuest.STATUS_CANCELLED)
.All(g => g.StatusCode == "401") ? new { code=501, name="預約" }:
_db.GuaDanOrderGuest
.Where(g => g.GuaDanOrderNo == a.GuaDanOrderNo && a.IsDeleted == false)
.Where(g => g.StatusCode != GuaDanOrderGuest.STATUS_CANCELLED)
.All(g => g.StatusCode == "403") ? new { code = 502, name = "全部退房" } :
_db.GuaDanOrderGuest
.Where(g => g.GuaDanOrderNo == a.GuaDanOrderNo && a.IsDeleted == false)
.Where(g => g.StatusCode != GuaDanOrderGuest.STATUS_CANCELLED)
.Any(g => g.StatusCode == "402" && a.IsCancel == false) ? new { code = 503, name = "正在入住" } :
new { code = 504, name = "部分退房" }
})
.Skip((search.page - 1) * search.pageSize)
.Take(search.pageSize)
.ToList();
return Ok(new
{
total,
data
});
}
[HttpGet]
[Route("api/guadan/getorderbyid")]
public async Task<IHttpActionResult> getGuadanOrderById(string orderId)
{
var order = await _db.GuaDanOrder.Where(a => a.GuaDanOrderNo == orderId).FirstOrDefaultAsync();
var order = await _db.GuaDanOrder
.Where(a => a.GuaDanOrderNo == orderId)
.Where(a => a.IsCancel == false && a.IsDeleted == false)
.FirstOrDefaultAsync();
if (order == null)
{
return BadRequest("未找到对应订单");
return BadRequest("未找到對應訂單");
}
var result = new
{
order.admin,
order.follower,
order.followers,
StartDate = order.StartDate?.ToString("yyyy-MM-dd"),
EndDate = order.EndDate?.ToString("yyyy-MM-dd"),
order.CreateUser,
@@ -59,6 +135,8 @@ public class guadanOrderController: ApiController
order.BookerPhone,
order.IsDeleted,
order.Uuid,
order.ActivityNum,
};
return Ok(result);
@@ -71,9 +149,9 @@ public class guadanOrderController: ApiController
{
return BadRequest("掛單資料不可為空");
}
if(model.Uuid.HasValue)
if (model.Uuid.HasValue)
{
return BadRequest("已存在对应挂单资料");
return BadRequest("已存在對應掛單資料");
}
try
{
@@ -99,6 +177,7 @@ public class guadanOrderController: ApiController
BookerName = model.bookerName,
BookerPhone = model.bookerPhone,
Uuid = Guid.NewGuid(),
ActivityNum = model.activityNum,
};
_db.GuaDanOrder.Add(guadanorder);
await _db.SaveChangesAsync();
@@ -131,26 +210,30 @@ public class guadanOrderController: ApiController
var order = await _db.GuaDanOrder.FindAsync(model.Uuid.Value);
if (order == null)
{
return BadRequest("未找到对应挂单资料");
return BadRequest("未找到對應掛單資料");
}
order.StartDate = model.startdate;
order.EndDate = model.enddate;
order.Notes = model.note;
order.BookerName = model.bookerName;
order.BookerPhone = model.bookerPhone;
order.ActivityNum = model.activityNum;
await _db.SaveChangesAsync();
return Ok(model);
}
[HttpPost]
[Route("api/guadan/delete")]
public async Task<IHttpActionResult> deleteGuadanOrder([FromUri] Guid uuid)
[Route("api/guadan/cancel")]
public async Task<IHttpActionResult> CancelGuadanOrder([FromUri] Guid uuid)
{
var guadan = await _db.GuaDanOrder.FindAsync(uuid);
if (guadan == null)
{
return NotFound();
}
if (_db.GuaDanOrderGuest.Any(a => (a.GuaDanOrderNo == guadan.GuaDanOrderNo) && a.StatusCode != "404"))
{
return BadRequest($"該掛單已經存在掛單蓮友,不能取消!");
}
using (var transaction = _db.Database.BeginTransaction())
{
try
@@ -158,31 +241,27 @@ public class guadanOrderController: ApiController
var guadanGuests = await _db.GuaDanOrderGuest
.Where(a => a.GuaDanOrderNo == guadan.GuaDanOrderNo)
.ToListAsync();
var scheduleIds = _db.RegionAndRoomAndBedSchedule
.Where(a => a.GuaDanOrderNo == guadan.GuaDanOrderNo)
.Where( b => b.IsActive == true)
.Select(c => c.GuaDanOrderNo)
.ToList();
if (guadanGuests.Any())
{
_db.GuaDanOrderGuest.RemoveRange(guadanGuests);
foreach (var guest in guadanGuests)
{
guest.StatusCode = "404";
// 取消所有相關的排程
if (guest.RegionAndRoomAndBedSchedule != null && guest.RegionAndRoomAndBedSchedule.Any())
{
foreach (var schedule in guest.RegionAndRoomAndBedSchedule)
{
schedule.IsCancel = true;
}
}
}
await _db.SaveChangesAsync();
}
if (scheduleIds.Any())
{
var schedules = await _db.RegionAndRoomAndBedSchedule
.Where(a => scheduleIds.Contains(a.GuaDanOrderNo))
.ToListAsync();
if (schedules.Any())
_db.RegionAndRoomAndBedSchedule.RemoveRange(schedules);
}
_db.GuaDanOrder.Remove(guadan);
guadan.IsCancel = true;
await _db.SaveChangesAsync();
transaction.Commit();
return Ok(new { message = "删除成功" });
return Ok(new { message = "取消成功" });
}
catch (Exception ex)
{
@@ -200,7 +279,6 @@ public class guadanOrderController: ApiController
public int guest_id { get; set; }
public DateTime start_date { get; set; }
public DateTime? end_date { get; set; }
public Guid? statusUuid { get; set; }
public int? create_user { get; set; }
public DateTime created_at { get; set; }
public DateTime updated_at { get; set; }
@@ -211,4 +289,14 @@ public class guadanOrderController: ApiController
public RegionRoomBed bed { get; set; } = null;
}
public class guadan_order_search_dto
{
public DateTime? startDate { get; set; }
public DateTime? endDate { get; set; }
public string guadanUser { get; set; }
public int page { get; set; } = 1;
public int pageSize { get; set; } = 10;
public string guaDanOrderNo { get; set; } = null;
}
}

View File

@@ -1,8 +1,12 @@
using Model;
using DocumentFormat.OpenXml.Drawing;
using Model;
using OfficeOpenXml.FormulaParsing.Excel.Functions.DateTime;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
@@ -11,7 +15,7 @@ using System.Web.Http;
/// guadanOrderGuest 的摘要描述
/// </summary>
[ezAuthorize]
public class guadanOrderGuestController: ApiController
public class guadanOrderGuestController : ApiController
{
private Model.ezEntities _db = new Model.ezEntities();
[HttpGet]
@@ -25,12 +29,12 @@ public class guadanOrderGuestController: ApiController
[Route("api/guadanorderguest/getbyorderno")]
public async Task<IHttpActionResult> getByOrderNo(string orderNo)
{
// 先查数据库,不做格式化
// 先查資料庫,不做格式化
var qry = await _db.GuaDanOrderGuest
.Where(a => a.GuaDanOrderNo == orderNo && a.IsDeleted == false)
.Where(a => a.GuaDanOrderNo == orderNo && a.IsDeleted == false && a.RegionRoomBedStatus.Code != "404")
.ToListAsync();
// 拉到内存后再处理日期
// 拉到記憶體後再處理日期
var data = qry.Select(a => new guadan_order_guest_display_dto
{
Uuid = a.Uuid,
@@ -42,27 +46,50 @@ public class guadanOrderGuestController: ApiController
checkoutat = a.CheckOutAt.HasValue ? a.CheckOutAt.Value.ToString("yyyy-MM-dd") : null,
phone = null,
roomName = a.Room.Name,
bedName = a.RegionRoomBed.Name,
bedName = GetBedString(a.RegionRoomBed),
orderNo = a.GuaDanOrderNo,
follower = a.follower,
statusUuid = a.statusUuid,
follower = a.followers == null ? null : new FollowerDto
{
num = a.followers.num,
u_name = a.followers.u_name,
sex = a.followers.sex
},
statuscode = a.StatusCode,
statusName = a.RegionRoomBedStatus?.Name,
}).ToList();
return Ok(data);
}
public string GetBedString(RegionRoomBed bed)
{
if (bed == null)
return "";
var room = bed.Room;
var name = room.Name + "/" + bed.Name;
var region = room.Region;
name = region.Name + "/" + name;
var parentRegion = region.Region2;
while(parentRegion != null)
{
name = parentRegion.Name + "/" + name;
parentRegion = parentRegion.Region2;
}
return name;
}
[HttpPost]
[Route("api/guadanorderguest/create")]
public async Task<IHttpActionResult> create([FromBody] guadan_order_guest_dto model)
{
if (model == null)
return BadRequest("");
/*if(model.statuscode == null)
{
return BadRequest("狀態不能為空");
}*/
// 驗證床位與蓮友
var bed = _db.RegionRoomBed.Find(model.bedUuid.Value);
if (model.followerNum.HasValue && model.bedUuid.HasValue)
{
var bed = _db.RegionRoomBed.Find(model.bedUuid.Value);
var follower = _db.followers.Find(model.followerNum.Value);
if (bed == null || follower == null)
@@ -81,16 +108,16 @@ public class guadanOrderGuestController: ApiController
}
if (!model.bedUuid.HasValue)
return BadRequest("床位 UUID 不能空");
return BadRequest("床位 UUID 不能空");
if (!model.checkInAt.HasValue)
return BadRequest("入住时间不能空");
return BadRequest("入住時間不能空");
// 長期占用處理checkOutAt 可為 null
DateTime? checkOut = model.checkOutAt.Value.Date;
if (checkOut.HasValue && model.checkInAt > checkOut)
return BadRequest("掛單結束時間不能再開始時間之前");
if(model.checkInAt == model.checkOutAt)
if (model.checkInAt == model.checkOutAt)
{
return BadRequest("掛單結束時間和開始時間不能是同一天");
}
@@ -103,8 +130,22 @@ public class guadanOrderGuestController: ApiController
);
if (!bedIsCanUse)
return BadRequest("床位在該時間段內已被占用");
if (model.followerNum.HasValue)
{
if (_db.GuaDanOrderGuest.Any(a => a.FollowerNum == model.followerNum
&& a.GuaDanOrderNo == model.orderNo
&& a.StatusCode != "404"
))
return BadRequest("該蓮友已經在該掛單中");
}
//建立訂單的的時候狀態只能是401或者402
var targetStatus = _db.RegionRoomBedStatus.Where(a => a.Code == GuaDanOrderGuest.STATUS_BOOKED).FirstOrDefault();
if (targetStatus == null)
{
return Content(HttpStatusCode.PreconditionFailed, "找不到目標狀態,請先建立對應狀態");
}
// 建立掛單
var guest = new GuaDanOrderGuest
{
GuaDanOrderNo = model.orderNo,
@@ -114,14 +155,9 @@ public class guadanOrderGuestController: ApiController
CheckInAt = model.checkInAt?.Date,
CheckOutAt = checkOut,
Uuid = Guid.NewGuid(),
statusUuid = model.statusUuid,
StatusCode = GuaDanOrderGuest.STATUS_BOOKED,
};
if (model.followerNum.HasValue)
{
if (_db.GuaDanOrderGuest.Any(a => a.FollowerNum == model.followerNum && a.GuaDanOrderNo == model.orderNo))
return BadRequest("該蓮友已經在該掛單中");
}
_db.GuaDanOrderGuest.Add(guest);
await _db.SaveChangesAsync();
@@ -140,12 +176,13 @@ public class guadanOrderGuestController: ApiController
Description = "床位掛單",
ScheduleDate = scheduleDate,
IsDeleted = false,
IsActive = true,
IsCancel = false,
TargetUuid = guest.BedUuid,
UseType = (int)RegionAndRoomAndBedSchedule.SchedulePurpose.Bed_Reservation,
CreatedAt = DateTime.Now,
GuaDanOrderNo = guest.GuaDanOrderNo,
Uuid = Guid.NewGuid()
Uuid = Guid.NewGuid(),
GuaDanOrderGuestUuid = guest.Uuid,
};
_db.RegionAndRoomAndBedSchedule.Add(schedul);
}
@@ -159,19 +196,17 @@ public class guadanOrderGuestController: ApiController
Description = "床位掛單(長期占用)",
ScheduleDate = null,
IsDeleted = false,
IsActive = true,
IsCancel = false,
TargetUuid = guest.BedUuid,
UseType = (int)RegionAndRoomAndBedSchedule.SchedulePurpose.Bed_Reservation,
CreatedAt = DateTime.Now,
GuaDanOrderNo = guest.GuaDanOrderNo,
Uuid = Guid.NewGuid()
Uuid = Guid.NewGuid(),
GuaDanOrderGuestUuid = guest.Uuid
};
_db.RegionAndRoomAndBedSchedule.Add(schedul);
}
await _db.SaveChangesAsync();
await _db.SaveChangesAsync();
return Ok();
}
@@ -201,10 +236,10 @@ public class guadanOrderGuestController: ApiController
}
if (!model.bedUuid.HasValue)
return BadRequest("床位 UUID 不能空");
return BadRequest("床位 UUID 不能空");
if (!model.checkInAt.HasValue)
return BadRequest("入住时间不能空");
return BadRequest("入住時間不能空");
// 長期占用處理
DateTime? checkOut = model.checkOutAt?.Date;
@@ -234,18 +269,27 @@ public class guadanOrderGuestController: ApiController
.AnyAsync();
if (exists) return BadRequest("該蓮友已經在該掛單中");
}
/*var targetStatus = _db.RegionRoomBedStatus.Find(model.statuscode);
if (targetStatus == null)
{
return BadRequest("目標狀態不存在");
}
if(!StatusTransitionManager.CanTransition(guest.StatusCode,targetStatus.Code))
{
return BadRequest("狀態的變化不合法");
}*/
// 更新掛單基本資料
guest.FollowerNum = model.followerNum;
guest.RoomUuid = model.roomUuid;
guest.BedUuid = model.bedUuid;
guest.CheckInAt = model.checkInAt?.Date;
guest.CheckOutAt = checkOut;
guest.statusUuid = model.statusUuid;
//guest.StatusCode = model.statuscode;
//更新的時候不能更新狀態狀態都用單獨的操作api控制
// 刪除原有每日排程
var oldSchedules = _db.RegionAndRoomAndBedSchedule
.Where(s => s.GuaDanOrderNo == guest.GuaDanOrderNo)
.Where(s => s.GuaDanOrderNo == guest.GuaDanOrderNo && s.TargetUuid == guest.BedUuid)
.ToList();
_db.RegionAndRoomAndBedSchedule.RemoveRange(oldSchedules);
@@ -262,12 +306,13 @@ public class guadanOrderGuestController: ApiController
Description = "床位掛單",
ScheduleDate = date,
IsDeleted = false,
IsActive = true,
IsCancel = false,
TargetUuid = guest.BedUuid,
UseType = (int)RegionAndRoomAndBedSchedule.SchedulePurpose.Bed_Reservation,
CreatedAt = DateTime.Now,
GuaDanOrderNo = guest.GuaDanOrderNo,
Uuid = Guid.NewGuid()
Uuid = Guid.NewGuid(),
GuaDanOrderGuestUuid = guest.Uuid,
};
_db.RegionAndRoomAndBedSchedule.Add(schedul);
}
@@ -281,12 +326,13 @@ public class guadanOrderGuestController: ApiController
Description = "床位掛單(長期占用)",
ScheduleDate = null,
IsDeleted = false,
IsActive = true,
IsCancel = false,
TargetUuid = guest.BedUuid,
UseType = (int)RegionAndRoomAndBedSchedule.SchedulePurpose.Bed_Reservation,
CreatedAt = DateTime.Now,
GuaDanOrderNo = guest.GuaDanOrderNo,
Uuid = Guid.NewGuid()
Uuid = Guid.NewGuid(),
GuaDanOrderGuestUuid = guest.Uuid,
};
_db.RegionAndRoomAndBedSchedule.Add(schedul);
}
@@ -296,27 +342,293 @@ public class guadanOrderGuestController: ApiController
await _db.SaveChangesAsync();
return Ok();
}
[HttpPost]
[Route("api/guadanorderguest/delete")]
public async Task<IHttpActionResult> deleteGuadanGuest([FromUri] Guid uuid)
[Route("api/guadanorderguest/xuzhu")]
public async Task<IHttpActionResult> ExtendStay([FromBody] XuZhuModel model)
{
//續住方法
if (model == null)
return BadRequest("請求數據為空");
if (model.GuestUuid == Guid.Empty || model.GuestBedUuid == Guid.Empty)
return BadRequest("GuestUuid 或 GuestBedUuid 無效");
var guest = await _db.GuaDanOrderGuest.FindAsync(model.GuestUuid);
if (guest == null)
{
return BadRequest("掛單不存在");
}
if (guest.BedUuid != model.GuestBedUuid)
{
return BadRequest("床位不正確");
}
var bedIsCanUse = await RegionAndRoomAndBedSchedule.IsBedAvailableAsync(_db, model.GuestBedUuid, model.CurrentCheckoutDate, model.NewCheckoutDate);
if (!bedIsCanUse)
{
return BadRequest("該床位在續住時間段內被預定,無法續住");
}
var newStartDate = model.CurrentCheckoutDate.Date;
var newEndDate = model.NewCheckoutDate.Date.AddDays(-1);
if (newEndDate < newStartDate)
return BadRequest("續住日期區間無效");
for (var date = newStartDate; date <= newEndDate; date = date.AddDays(1))
{
var newSchedule = new RegionAndRoomAndBedSchedule
{
GuaDanOrderNo = guest.GuaDanOrderNo,
Uuid = Guid.NewGuid(),
TargetUuid = model.GuestBedUuid,
GuaDanOrderGuestUuid = model.GuestUuid,
ScheduleDate = date,
Title = "續住掛單", // 一天一條,開始和結束是同一天
Description = "續住掛單",
UseType = 30,
CreatedAt = DateTime.UtcNow
};
_db.RegionAndRoomAndBedSchedule.Add(newSchedule);
}
guest.CheckOutAt = model.NewCheckoutDate.Date;
await _db.SaveChangesAsync(); // 保存資料庫操作
return Ok(new { message = "續住成功" });
}
[HttpPost]
[Route("api/guadanorderguest/cancel")]
public async Task<IHttpActionResult> CancelGuadanGuest([FromUri] Guid uuid)
{
var guest = await _db.GuaDanOrderGuest.FindAsync(uuid);
if (guest == null)
return BadRequest("未找到指定挂单资料");
return NotFound();
// 删除所有与该 guest 相关的排程(每日排程或長期占用)
var schedules = _db.RegionAndRoomAndBedSchedule
.Where(s => s.GuaDanOrderNo == guest.GuaDanOrderNo)
.ToList();
if (guest.StatusCode == "404")
return BadRequest("該掛單已取消,無需再取消");
if (schedules.Any())
_db.RegionAndRoomAndBedSchedule.RemoveRange(schedules);
if (!StatusTransitionManager.CanTransition(guest.StatusCode, "404"))
{
return BadRequest("當前狀態不能取消");
}
var cancelStatus = await _db.RegionRoomBedStatus
.FirstOrDefaultAsync(a => a.Code == "404");
if (cancelStatus == null)
return Content(HttpStatusCode.PreconditionFailed, "找不到取消狀態Code=404請先建立對應狀態");
//把狀態設置為取消
using (var tx = _db.Database.BeginTransaction())
{
try
{
// 1) 更新 guest
guest.StatusCode = "404";
_db.GuaDanOrderGuest.Remove(guest);
await _db.SaveChangesAsync();
// 2) 取消相關排程(常見做法:只取消未來&未取消的)
var today = DateTime.Today;
return Ok(new { message = "删除成功" });
var schedules = await _db.RegionAndRoomAndBedSchedule
.Where(s => s.GuaDanOrderGuestUuid == guest.Uuid
&& s.IsCancel == false
//&& s.ScheduleDate >= today
)
// ✅ 只取消今天與未來的,能取消就代表未入住,
// 未入住就要全部取消,如果是入住後提前退房,
// 就要取消未來的,取消未來的時候要注意今日是否包含的問題
.ToListAsync();
foreach (var s in schedules)
{
s.IsCancel = true;
}
// 3) 釋放占用資源(若有床位/房間占用紀錄)
if (guest.BedUuid != null)
{
// 先抓到目標狀態
var freeStatus = await _db.RegionRoomBedStatus
.FirstOrDefaultAsync(a => a.Code == "101");
if (freeStatus == null)
return Content(HttpStatusCode.PreconditionFailed, "找不到床位狀態 Code=101");
if (guest.RegionRoomBed != null)
{
guest.RegionRoomBed.StatusCode = freeStatus.Code;
}
}
await _db.SaveChangesAsync();
tx.Commit();
return Ok(new
{
message = "取消成功",
guestUuid = guest.Uuid,
canceledSchedules = schedules.Count
});
}
catch (DbUpdateConcurrencyException)
{
tx.Rollback();
return StatusCode(HttpStatusCode.PreconditionFailed); // 或自訂訊息
}
catch (Exception ex)
{
tx.Rollback();
return InternalServerError(ex);
}
}
}
[HttpPost]
[Route("api/guadanorderguest/checkout")]
public IHttpActionResult CheckoutGuadanOrderGuest(Guid uuid)
{
DbContextTransaction transaction = null;
try
{
transaction = _db.Database.BeginTransaction(); // 開啟事務
// 1⃣ 取得該筆掛單
var guest = _db.GuaDanOrderGuest
.Where(a => a.Uuid == uuid)
.Where(a => !a.IsDeleted && a.StatusCode != "404")
.FirstOrDefault();
if (guest == null)
return NotFound();
// 2⃣ 標記為已退房
var targetStatus = _db.RegionRoomBedStatus
.Where(a => a.Code == "403")
.FirstOrDefault();
if (targetStatus == null)
return Content(HttpStatusCode.PreconditionFailed, "找不到退房狀態Code=403請先建立對應狀態");
if (!StatusTransitionManager.CanTransition(guest.StatusCode, targetStatus.Code))
return BadRequest("掛單狀態轉換不對");
if (!StatusTransitionManager.CanTransition(guest.RegionRoomBed.StatusCode, "101"))
return BadRequest("床位掛單狀態轉換不對");
guest.StatusCode = targetStatus.Code;
guest.RegionRoomBed.StatusCode = "101";
//更新未來排程為取消
var latestCheckoutStr = _db.GuadanTimeSetting
.Select(a => a.LatestCheckOut) // 字串 "HH:mm"
.FirstOrDefault();
TimeSpan? latestCheckoutTime = null;
if (!string.IsNullOrEmpty(latestCheckoutStr))
{
// 嘗試解析字串
if (TimeSpan.TryParse(latestCheckoutStr, out var ts))
{
latestCheckoutTime = ts;
}
}
var futureSchedules = _db.RegionAndRoomAndBedSchedule
.Where(s => s.GuaDanOrderGuestUuid == guest.Uuid);
if (latestCheckoutTime == null || DateTime.Now.TimeOfDay > latestCheckoutTime)
{
// 包含今天
futureSchedules = futureSchedules.Where(s => s.ScheduleDate > DateTime.Today);
}
else
{
// 不包含今天
futureSchedules = futureSchedules.Where(s => s.ScheduleDate >= DateTime.Today);
}
foreach (var schedule in futureSchedules)
{
schedule.IsCancel = true;
}
// 4⃣ 保存所有變更
_db.SaveChanges();
// 5⃣ 提交事務
transaction.Commit();
return Ok(new { message = "退房完成", guestUuid = guest.Uuid });
}
catch (Exception ex)
{
if (transaction != null)
transaction.Rollback(); // 回滾事務
return InternalServerError(ex);
}
finally
{
if (transaction != null)
transaction.Dispose();
}
}
[HttpPost]
[Route("api/guadanorderguest/checkin")]
public IHttpActionResult CheckinGuadanGuest([FromUri] Guid uuid)
{
if (uuid == Guid.Empty)
return BadRequest("uuid不能為空");
// 獲取掛單客人
var guest = _db.GuaDanOrderGuest
.Include(g => g.RegionRoomBedStatus) // 包含導航屬性
.FirstOrDefault(g => g.Uuid == uuid);
if (guest == null)
return NotFound();
string currentStatus = guest.StatusCode;
// 判斷狀態流轉是否合法
if (!StatusTransitionManager.CanTransition(currentStatus, GuaDanOrderGuest.STATUS_CHECKED_IN))
{
return BadRequest("當前狀態不允許入住");
}
// ---------- 新增:檢查今天是否在排程表 ----------
var today = DateTime.Today;
bool hasScheduleToday = _db.RegionAndRoomAndBedSchedule
.Any(s => s.GuaDanOrderGuestUuid == guest.Uuid && s.ScheduleDate == today);
if (!hasScheduleToday)
{
return BadRequest("不在入住時間段內,無法入住");
}
try
{
// 更新狀態
guest.StatusCode = GuaDanOrderGuest.STATUS_CHECKED_IN;
// 如果需要,更新床位狀態,比如變為占用
if (guest.BedUuid != null)
{
var bed = _db.RegionRoomBed.FirstOrDefault(b => b.Uuid == guest.BedUuid);
if (bed == null)
{
return BadRequest("入住床位不存在");
}
if (StatusTransitionManager.CanTransition(bed.StatusCode, "102")) // 102 = 占用
{
bed.StatusCode = "102";
}
else
{
return BadRequest($"當前床位狀態:{bed.RegionRoomBedStatus.Name} 不能入住");
}
}
else if (guest.BedUuid == null)
{
return BadRequest("入住床位不存在");
}
_db.SaveChanges();
return Ok(new { message = "入住成功", statusCode = guest.StatusCode });
}
catch (Exception ex)
{
return InternalServerError(ex);
}
}
public class guadan_order_guest_dto
@@ -328,7 +640,7 @@ public class guadanOrderGuestController: ApiController
public Guid? bedUuid { get; set; }
public DateTime? checkInAt { get; set; }
public DateTime? checkOutAt { get; set; }
public Guid? statusUuid { get; set; }
public string statuscode { get; set; }
}
public class guadan_order_guest_display_dto
{
@@ -337,17 +649,31 @@ public class guadanOrderGuestController: ApiController
public string orderNo { get; set; }
public string name { get; set; }
public Guid? roomUuid { get; set; }
public Guid? bedUuid { get;set; }
public string checkinat { get;set; }
public string checkoutat { get;set; }
public int? gender { get; set; }
public Guid? statusUuid { get; set; }
public Guid? bedUuid { get; set; }
public string checkinat { get; set; }
public string checkoutat { get; set; }
public int? gender { get; set; }
public string statuscode { get; set; }
public string statusName { get; set; }
public string phone { get; set; }
public string note { get; set; }
public string roomName { get; set; }
public string bedName { get; set; }
public follower follower { get; set; }
public bool iscancel { get; set; }
public FollowerDto follower { get; set; }
}
public class FollowerDto
{
public int num { get; set; }
public string u_name { get; set; }
public string sex { get; set; }
}
public class XuZhuModel
{
public Guid GuestUuid { get; set; } // 不可為空
public Guid GuestBedUuid { get; set; } // 不可為空
public DateTime CurrentCheckoutDate { get; set; } // 當前退房時間
public DateTime NewCheckoutDate { get; set; } // 新退房時間
}
}

View File

@@ -21,39 +21,49 @@ public class guadanStatisticsController: ApiController
//挂单统计:房间,床位,挂单笔数,挂单人数的统计
var now = DateTime.Now;
var roomCount = await _db.Room.Where(a => a.IsDeleted == false).CountAsync();
var rooms = await _db.Room.Include(r => r.RegionRoomBed).ToListAsync();
var emptyRoomCount = rooms
.Where(r => r.RegionRoomBed.All(b => b.IsAvailableDuring(now, now, _db))) // 這裡就能用方法
.Count();
var bedCount = await _db.RegionRoomBed.Where(a => a.IsDeleted == false).CountAsync();
var maleBedCount = await _db.RegionRoomBed.Where(a => a.IsDeleted == false && a.Gender == true).CountAsync();
var femaleBedCount = await _db.RegionRoomBed.Where(a => a.IsDeleted == false && a.Gender == false).CountAsync();
var guadanTotalCount = await _db.GuaDanOrder.Where(a => a.IsDeleted == false).CountAsync();
var guadanPeopleTotal = await _db.GuaDanOrderGuest.Where(a => a.IsDeleted == false).CountAsync();
var guadanPeopleMale = await _db.GuaDanOrderGuest.Where(a => a.IsDeleted == false && a.follower.sex == "男眾").CountAsync();
var guadanPeopleFemale = await _db.GuaDanOrderGuest.Where(a => a.IsDeleted == false && a.follower.sex == "女眾").CountAsync();
dynamic bedCounts = await RegionAndRoomAndBedSchedule.GetAvailableBedCountsAsync(_db, DateTime.Now, DateTime.Now);
var guadanCurrentCount = await _db.GuaDanOrder.Where(a => now < a.EndDate).CountAsync();
var guadanPeopleCurrent = await _db.GuaDanOrderGuest.Where( a => a.CheckOutAt > now).CountAsync();
var guadanPeopleCurrentMale = await _db.GuaDanOrderGuest.Where(a => a.CheckOutAt > now && a.follower.sex == "男眾").CountAsync();
var guadanPeopleCurrentFemale = await _db.GuaDanOrderGuest.Where(a => a.CheckOutAt > now && a.follower.sex == "女眾").CountAsync();
var guadanTotalCount = await _db.GuaDanOrder
.Where(a => a.IsDeleted == false)
.Where(a => a.IsCancel == false)
.CountAsync();
var guadanCurrentCount = await _db.GuaDanOrderGuest
.Where(guest => guest.StatusCode != "403")
.Where(guest => guest.StatusCode != "404")
.Select(guest => guest.GuaDanOrderNo)
.Distinct()
.CountAsync();
var guadanPeopleTotal = await _db.GuaDanOrderGuest
.Where(a => a.IsDeleted == false)
.Where(guest => guest.StatusCode != "404")
.CountAsync();
var guadanPeopleMale = await _db.GuaDanOrderGuest
.Where(guest => guest.StatusCode != "404")
.Where(a => a.IsDeleted == false && a.followers.sex == "男眾")
.CountAsync();
var guadanPeopleFemale = await _db.GuaDanOrderGuest
.Where(guest => guest.StatusCode != "404")
.Where(a => a.IsDeleted == false && a.followers.sex == "女眾")
.CountAsync();
var guadanPeopleCurrent = await _db.GuaDanOrderGuest
.Where(a => a.IsDeleted == false)
.Where(guest => guest.StatusCode != "404")
.Where(guest => guest.StatusCode != "403")
.Where( a => a.CheckOutAt >= now.Date)
.CountAsync();
var guadanPeopleCurrentMale = await _db.GuaDanOrderGuest
.Where(a => a.IsDeleted == false)
.Where(guest => guest.StatusCode != "404")
.Where(guest => guest.StatusCode != "403")
.Where(a => a.CheckOutAt >= now.Date && a.followers.sex == "男眾")
.CountAsync();
var guadanPeopleCurrentFemale = await _db.GuaDanOrderGuest
.Where(a => a.IsDeleted == false)
.Where(guest => guest.StatusCode != "404")
.Where(guest => guest.StatusCode != "403")
.Where(a => a.CheckOutAt >= now.Date && a.followers.sex == "女眾")
.CountAsync();
var result = new
{
roomStatistics = new
{
roomCount = roomCount,
emptyRoomCount = emptyRoomCount,
bedCount = bedCount,
maleBedCount = maleBedCount,
femaleBedCount = femaleBedCount,
emptyBedCount = bedCounts.male + bedCounts.female,
emptyMaleBedCount = bedCounts.male,
emptyFemaleBedCount = bedCounts.female
},
guadanStatistics = new
{
guadanTotalCount = guadanTotalCount, // 总挂单次数
@@ -61,7 +71,7 @@ public class guadanStatisticsController: ApiController
guadanPeopleTotal = guadanPeopleTotal, // 总挂单人数
guadanPeopleMale = guadanPeopleMale,
guadanPeopleFemale = guadanPeopleFemale,
guadanPeopleCurrent = guadanPeopleCurrent, // 当前挂单人数
guadanPeopleCurrent = guadanPeopleCurrent, // 已預約掛單人數
guadanPeopleCurrentMale = guadanPeopleCurrentMale,
guadanPeopleCurrentFemale = guadanPeopleCurrentFemale
}

View File

@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
/// <summary>
/// guadanStatisticsTable 的摘要描述
/// </summary>
///
[ezAuthorize]
public class guadanStatisticsTableController: ApiController
{
private Model.ezEntities _db = new Model.ezEntities();
public guadanStatisticsTableController()
{
//
// TODO: 在這裡新增建構函式邏輯
//
}
[HttpGet]
[Route("api/guadan/guadanstatisticstable/list")]
public IHttpActionResult Get([FromUri] DateTime? start, [FromUri] DateTime? end)
{
// 如果兩個都為空,設定 start = 今天end = 一個月後
if (!start.HasValue && !end.HasValue)
{
start = DateTime.Today;
end = DateTime.Today.AddMonths(1);
}
// 如果有 start 沒有 end就給 end 預設一個月後
if (start.HasValue && !end.HasValue)
{
end = start.Value.Date.AddMonths(1);
}
// 如果有 end 沒有 start就給 start 預設 end 的前一個月
if (!start.HasValue && end.HasValue)
{
start = end.Value.Date.AddMonths(-1);
}
// 最後確保只取 date 部分
var startDate = start.Value.Date;
var endDate = end.Value.Date;
// 查詢資料庫時就用 date 型別對應
var statistics = _db.RegionAndRoomAndBedSchedule
.Where(s => s.IsCancel == false)
.Where(s => s.ScheduleDate >= startDate && s.ScheduleDate <= endDate)
.GroupBy(s => s.ScheduleDate)
.Select(g => new
{
date = g.Key,
todaytotalbookers = g.Count(), // 该日期的总记录数
checkin = g.Key <= DateTime.Today
? g.Count(x => x.GuaDanOrderGuest.RegionRoomBedStatus.Code == "402"
|| x.GuaDanOrderGuest.RegionRoomBedStatus.Code == "403")
: 0,
checkinfemale = g.Key <= DateTime.Today
? g.Count(x => (x.GuaDanOrderGuest.RegionRoomBedStatus.Code == "402"
|| x.GuaDanOrderGuest.RegionRoomBedStatus.Code == "403")
&& x.GuaDanOrderGuest.followers.sex == "女眾")
: 0,
checkinmale = g.Key <= DateTime.Today
? g.Count(x => (x.GuaDanOrderGuest.RegionRoomBedStatus.Code == "402"
|| x.GuaDanOrderGuest.RegionRoomBedStatus.Code == "403")
&& x.GuaDanOrderGuest.followers.sex == "男眾")
: 0,
bookfemale = g.Count(x => x.GuaDanOrderGuest.followers.sex == "女眾"),
bookmale = g.Count(x => x.GuaDanOrderGuest.followers.sex == "男眾")
})
.OrderBy(x => x.date)
.ToList();
var todayDate = DateTime.Today;
var bedcount = _db.RegionRoomBed
.Where(a => a.IsDeleted == false)
.Count();
var roomcount = _db.Room.Count();
var result = new
{
bedcount,
roomcount,
statistics,
};
return Ok(result);
}
}

View File

@@ -11,7 +11,7 @@ using System.Web.Http;
/// lianyouController 的摘要描述
/// </summary>
[ezAuthorize]
public class lianyouController: ApiController
public class lianyouController : ApiController
{
private Model.ezEntities _db = new Model.ezEntities();
@@ -19,13 +19,34 @@ public class lianyouController: ApiController
[Route("api/lianyou/getfollowers")]
public async Task<IHttpActionResult> GetGuadanFollowers(int page, int pageSize, string searchName = null)
{
var qry = _db.followers.AsEnumerable();
if(searchName != null)
// IQueryable 可讓 EF 在資料庫層面執行過濾和投影
var qry = _db.followers.AsQueryable();
if (!string.IsNullOrEmpty(searchName))
{
qry = qry.Where(a => (a.u_name ?? "").Contains(searchName) || (a.phone ?? "").Contains(searchName));
}
var count = qry.Count();
var data = qry.OrderBy(a => a.f_number).ToPagedList(page, pageSize);
return Ok(new {data = data, count = count});
var count = await qry.CountAsync();
// 投影到只需要的欄位
var projected = qry
.OrderBy(a => a.f_number)
.Select(a => new
{
num = a.num,
u_name = a.u_name,
phone = a.phone,
sex = a.sex,
});
// 分頁
var data = projected
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList(); // 如果使用 EF Core 可用 ToListAsync()
return Ok(new { data = data, count = count });
}
}

View File

@@ -22,94 +22,14 @@ public class orderdetailController:ApiController
public IHttpActionResult GetList([FromBody] Model.ViewModel.pro_order q, int page, int pageSize = 10, string sortBy = "", bool sortDesc = false)
{
int activity_num = Convert.ToInt32(q.activity_num);
//現在的牌位預覽只會出現功德主,修改為所有人都會出現
//var aIDt = _db.actItems.AsEnumerable().Where(f => f.subject.Contains(q.actItemTxt.Trim())).Select(f => f.num);//品項
var OrderList = _db.pro_order.Where(u => u.activity_num == activity_num).Select(j => j.order_no).ToList();
var gdzOrderList = _db.pro_order_detail.Where(o => OrderList.Contains(o.order_no) && o.print_id.Contains("主")).Select(o => o.order_no).Distinct().ToList();
var qry = _db.pro_order.Where(u => gdzOrderList.Contains(u.order_no)).AsEnumerable();
if (!string.IsNullOrEmpty(q.order_no))
qry = qry.Where(o => o.order_no.Contains(q.order_no.Trim()));
if (!string.IsNullOrEmpty(q.keyin1))
qry = qry.Where(o => o.keyin1.Contains(q.keyin1));
if (q.f_num.HasValue && q.f_num > 0)
qry = qry.Where(o => o.f_num == q.f_num);
if (q.activity_num.HasValue && q.activity_num > 0)
qry = qry.Where(o => o.activity_num == q.activity_num);
if (q.up_time1.HasValue)
qry = qry.Where(o => o.up_time >= q.up_time1.Value);
if (q.up_time2.HasValue)
qry = qry.Where(o => o.up_time < Convert.ToDateTime(q.up_time2.Value).AddDays(1));
if (!string.IsNullOrEmpty(q.address))
qry = qry.Where(o => o.address.Contains(q.address.Trim()));
if (!string.IsNullOrEmpty(q.subject))
qry = qry.Where(o => o.activity_num.HasValue && o.activity.subject.Contains(q.subject?.Trim()));
if (!string.IsNullOrEmpty(q.u_name))
qry = qry.Where(o => o.f_num.HasValue && o.follower.u_name.Contains(q.u_name?.Trim()));
if (!string.IsNullOrEmpty(q.introducerTxt))
qry = qry.Where(o => o.introducer.HasValue && o.follower1.u_name.Contains(q.introducerTxt?.Trim()));
if (!string.IsNullOrEmpty(q.actItemTxt))
{
//qry = qry.Where(o => o.pro_order_detail.Where(f2 => f2.order_no == o.order_no && aIDt.ToArray().Contains(f2.actItem_num?.ToString())).Count() > 0);
// qry = qry.Where(o => o.pro_order_detail.Where(f2 => f2.order_no == o.order_no && aIDt.Any(x => x == f2.actItem_num)).Count() > 0);
qry = qry.Where(o => o.pro_order_detail.Where(f2 => f2.actItem_num.HasValue && f2.actItem.subject.Contains(q.actItemTxt?.Trim())).Count() > 0);
}
if (!string.IsNullOrEmpty(q.country))
qry = qry.Where(o => o.f_num != null && o.follower?.country == q.country);
if (!string.IsNullOrEmpty(q.country2))
{
if (q.country2 == "1")
{
qry = qry.Where(o => o.f_num != null && o.follower?.country == "158");
}
else if (q.country2 == "2")
{
qry = qry.Where(o => o.f_num != null && o.follower?.country != "158");
}
}
if (sortBy.Equals("order_no"))
{
if (sortDesc)
qry = qry.OrderByDescending(o => o.order_no);
else
qry = qry.OrderBy(o => o.order_no);
}
else if (sortBy.Equals("keyin1_txt"))
{
if (sortDesc)
qry = qry.OrderByDescending(o => o.keyin1);
else
qry = qry.OrderBy(o => o.keyin1);
}
else if (sortBy.Equals("up_time"))
{
if (sortDesc)
qry = qry.OrderByDescending(o => o.up_time);
else
qry = qry.OrderBy(o => o.up_time);
}
else if (sortBy.Equals("u_name"))
{
if (sortDesc)
qry = qry.OrderByDescending(o => o.follower?.u_name);
else
qry = qry.OrderBy(o => o.follower?.u_name);
}
else if (sortBy.Equals("subject"))
{
if (sortDesc)
qry = qry.OrderByDescending(o => o.activity?.subject);
else
qry = qry.OrderBy(o => o.activity?.subject);
}
else
qry = qry.OrderByDescending(o => o.reg_time);
//var OrderList = _db.pro_order.Where(u => u.activity_num == activity_num).Select(j => j.order_no).ToList();
//var gdzOrderList = _db.pro_order_detail.Where(o => OrderList.Contains(o.order_no) && o.print_id.Contains("主")).Select(o => o.order_no).Distinct().ToList();
//var qry = _db.pro_order.Where(u => gdzOrderList.Contains(u.order_no)).AsEnumerable();
var qry = _db.pro_order.Where( u => u.activity_num == activity_num).AsEnumerable();
qry = qry.OrderByDescending(o => o.reg_time);
var count = qry.Count(); //pageSize = count;//一次取回??
var ret = new
@@ -125,7 +45,11 @@ public class orderdetailController:ApiController
up_time = x.up_time,
keyin1_txt = Model.pro_order.keyin1_value_to_text(x.keyin1),
//detail = x.pro_order_detail.Where(u => u.printed_files != null)
detail = new { count = x.pro_order_detail.Where(u => u.actItem.act_bom.Count() == 0).Count(),
detail = new { count = x.pro_order_detail
.Where(u => (u.parent_num != null)
|| u.actItem.subject.Contains("牌")
|| !string.IsNullOrEmpty(u.f_num_tablet))
.Count(),
actItem = x.pro_order_detail.Where(u => u.printed_files != null).FirstOrDefault()?.print_id }
}),
count = count

View File

@@ -0,0 +1,250 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Web.Http;
using System.Configuration;
/// <summary>
/// pivot01Controller - 法會報名統計分析 API
/// 設計理念:直接查詢 SQL VIEW保持彈性與簡潔
/// </summary>
[ezAuthorize]
public class pivot01Controller : ApiController
{
// 連線字串
private readonly string _connectionString;
public pivot01Controller()
{
// 優先使用 shopConn 連線字串(純 SQL Server 連線字串)
var shopConnectionString = ConfigurationManager.ConnectionStrings["shopConn"]?.ConnectionString;
if (!string.IsNullOrEmpty(shopConnectionString))
{
// 移除不相容的 Provider 參數(包含 SQLOLEDB 和 SQLNCLI11
_connectionString = shopConnectionString
.Replace("Provider=SQLOLEDB;", "")
.Replace("Provider=SQLNCLI11;", "")
.Replace(" Provider=SQLOLEDB", "")
.Replace(" Provider=SQLNCLI11", "");
}
else
{
// 備用方案:從 Entity Framework 連線字串中提取 SQL Server 連線字串
var efConnectionString = ConfigurationManager.ConnectionStrings["ezEntities"]?.ConnectionString;
if (!string.IsNullOrEmpty(efConnectionString))
{
// 解析 EF 連線字串,提取 provider connection string 部分
var startIndex = efConnectionString.IndexOf("provider connection string=&quot;") + "provider connection string=&quot;".Length;
var endIndex = efConnectionString.LastIndexOf("&quot;");
if (startIndex > 0 && endIndex > startIndex)
{
_connectionString = efConnectionString.Substring(startIndex, endIndex - startIndex);
}
else
{
throw new InvalidOperationException("無法解析 Entity Framework 連線字串");
}
}
else
{
throw new InvalidOperationException("找不到可用的資料庫連線字串");
}
}
}
#region VIEW
/// <summary>
/// 執行 SQL 查詢並回傳 DataTable
/// </summary>
/// <param name="sql">SQL 查詢語句</param>
/// <param name="parameters">參數陣列</param>
/// <returns>查詢結果 DataTable</returns>
private DataTable ExecuteSqlQuery(string sql, SqlParameter[] parameters = null)
{
var dataTable = new DataTable();
try
{
using (var connection = new SqlConnection(_connectionString))
using (var command = new SqlCommand(sql, connection))
{
// 設定逾時時間為 60 秒
command.CommandTimeout = 60;
// 加入參數
if (parameters != null && parameters.Length > 0)
{
command.Parameters.AddRange(parameters);
}
// 開啟連線並執行查詢
connection.Open();
using (var adapter = new SqlDataAdapter(command))
{
adapter.Fill(dataTable);
}
}
}
catch (SqlException ex)
{
throw new Exception($"SQL 查詢錯誤: {ex.Message}", ex);
}
catch (Exception ex)
{
throw new Exception($"執行查詢時發生錯誤: {ex.Message}", ex);
}
return dataTable;
}
/// <summary>
/// DataTable 轉換為動態物件列表(保留中文欄位名)
/// </summary>
/// <param name="dt">DataTable</param>
/// <returns>Dictionary 列表</returns>
private List<Dictionary<string, object>> DataTableToDictionary(DataTable dt)
{
var list = new List<Dictionary<string, object>>();
foreach (DataRow row in dt.Rows)
{
var dict = new Dictionary<string, object>();
foreach (DataColumn col in dt.Columns)
{
// 保留原始欄位名稱(包含中文)
dict[col.ColumnName] = row[col] == DBNull.Value ? null : row[col];
}
list.Add(dict);
}
return list;
}
#endregion
#region API
/// <summary>
/// GET /api/pivot01/activity_stats
/// 查詢法會統計對應「法會統計」VIEW
/// </summary>
/// <param name="year">查詢年份</param>
/// <returns>法會統計資料</returns>
[HttpGet]
[Route("api/pivot01/activity_stats")]
public IHttpActionResult GetActivityStats(int year)
{
try
{
// 驗證年份參數
if (year < 1900 || year > 2100)
{
return BadRequest("年份參數不正確,請輸入 1900 ~ 2100 之間的年份");
}
// 建立 SQL 查詢(包含當年度及無日期的法會)
string sql = @"
SELECT * FROM [法會統計]
WHERE (YEAR(開始日期) = @year OR 開始日期 IS NULL)
ORDER BY
CASE WHEN 開始日期 IS NULL THEN 1 ELSE 0 END,
開始日期 DESC,
結束日期 DESC
";
// 建立參數
var parameters = new[]
{
new SqlParameter("@year", SqlDbType.Int) { Value = year }
};
// 執行查詢
var dataTable = ExecuteSqlQuery(sql, parameters);
var data = DataTableToDictionary(dataTable);
// 回應結果
var result = new
{
success = true,
data = data,
message = "查詢成功",
rowCount = data.Count
};
return Ok(result);
}
catch (Exception ex)
{
var errorResponse = new
{
success = false,
message = $"查詢失敗:{ex.Message}",
error = ex.ToString()
};
return Content(System.Net.HttpStatusCode.BadRequest, errorResponse);
}
}
/// <summary>
/// GET /api/pivot01/registration_details
/// 查詢報名明細對應「報名明細查詢」VIEW
/// </summary>
/// <param name="activityNum">法會編號(必填)</param>
/// <returns>報名明細資料</returns>
[HttpGet]
[Route("api/pivot01/registration_details")]
public IHttpActionResult GetRegistrationDetails(int? activityNum = null)
{
try
{
// 驗證參數
if (!activityNum.HasValue || activityNum.Value <= 0)
{
return BadRequest("請提供有效的法會編號activityNum");
}
// 建立查詢 SQL
string sql = @"
SELECT * FROM [報名明細查詢]
WHERE 法會ID = @activityNum
ORDER BY 報名日期 DESC, 報名編號 DESC
";
// 建立參數
var parameters = new[]
{
new SqlParameter("@activityNum", SqlDbType.Int) { Value = activityNum.Value }
};
// 執行查詢
var dataTable = ExecuteSqlQuery(sql, parameters);
var data = DataTableToDictionary(dataTable);
// 回應結果
var result = new
{
success = true,
data = data,
message = "查詢成功",
rowCount = data.Count
};
return Ok(result);
}
catch (Exception ex)
{
var errorResponse = new
{
success = false,
message = $"查詢失敗:{ex.Message}",
error = ex.ToString()
};
return Content(System.Net.HttpStatusCode.BadRequest, errorResponse);
}
}
#endregion
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Http;
using System.Web.Routing;
@@ -11,7 +12,7 @@ using System.Web.Routing;
/// regionController 的摘要描述
/// </summary>
[ezAuthorize]
public class regionController: ApiController
public class regionController : ApiController
{
private Model.ezEntities _db = new Model.ezEntities();
public regionController()
@@ -39,8 +40,9 @@ public class regionController: ApiController
var startDate = filter.StartDate.Date;
var endDate = filter.EndDate.Date;
var query = _db.Region
var query = _db.Region//區域狀態是否啟用這裡只設置了過濾了有客房的區域,是否要過濾所有
.Where(r => !r.IsDeleted)
.Where(r => r.IsActive)
.Where(r => r.Room.Any());
if (filter.Gender != null)
@@ -67,6 +69,7 @@ public class regionController: ApiController
r.Uuid,
r.Name,
regionPath = r.Name,
isStop = !IsRegionAvailable(r.Uuid),
Room = r.Room
.Where(room => filter.Gender == null || room.Gender == filter.Gender)
.Where(room =>
@@ -95,17 +98,19 @@ public class regionController: ApiController
bed.Name,
bed.Gender,
bed.RoomUuid,
bed.StatusUuid,
bed.StatusCode,
bed.IsActive,
bed.IsDeleted,
canuse = bed.IsAvailableDuring(startDate, endDate, _db),
statusname = bed.RegionRoomBedStatus.Name,
schedules = _db.RegionAndRoomAndBedSchedule
.Where(s => s.IsCancel == false)
.Where(s => s.TargetUuid == bed.Uuid
&& s.IsDeleted == false
&& s.IsActive
&& (s.ScheduleDate == null
|| (s.ScheduleDate >= startDate && s.ScheduleDate <= endDate)))
|| (s.ScheduleDate >= startDate)))
.Where(s => s.GuaDanOrderGuest.StatusCode != "403" && s.GuaDanOrderGuest.StatusCode != "404")
.OrderBy(a => a.ScheduleDate)
.Select(s => new
{
s.Uuid,
@@ -113,7 +118,15 @@ public class regionController: ApiController
s.UseType,
s.Title,
s.Description,
s.GuaDanOrderNo
s.GuaDanOrderNo,
s.TargetUuid,
usename = _db.GuaDanOrderGuest
.Where(guest => guest.GuaDanOrderNo == s.GuaDanOrderNo)
.Where(guest => guest.BedUuid == s.TargetUuid)
.Select(guest => guest.followers.u_name)
.FirstOrDefault()
})
.ToList()
}),
@@ -138,6 +151,30 @@ public class regionController: ApiController
Summary = summary,
});
}
public bool IsRegionAvailable(Guid regionUuid)
{
var current = _db.Region.FirstOrDefault(r => r.Uuid == regionUuid);
while (current != null)
{
// 當前區域不可用就直接返回 false
if (!current.IsActive || current.IsDeleted)
{
return false;
}
// 沒有父區域了,說明一路上都可用
if (!current.ParentUuid.HasValue)
{
return true;
}
// 繼續往父區域走
current = _db.Region.FirstOrDefault(r => r.Uuid == current.ParentUuid.Value);
}
// 沒查到(極端情況,比如資料庫被改了)
return false;
}
/// <summary>
/// 遞迴生成區域完整路徑
@@ -183,7 +220,8 @@ public class regionController: ApiController
IsActive = region.IsActive,
RoomCount = region.RoomCount,
BedDto = new List<BedDto>(),
Rooms = region.Room.Select(a => new RoomDto {
Rooms = region.Room.Select(a => new RoomDto
{
Uuid = a.Uuid,
Name = a.Name,
RegionUuid = a.RegionUuid,
@@ -194,13 +232,13 @@ public class regionController: ApiController
{
Uuid = c.Uuid,
name = c.Name,
roomUuid = c.RoomUuid,
roomUuid = c.RoomUuid,
isactive = c.IsActive,
statusuuid = c.StatusUuid,
statuscode = c.StatusCode,
Gender = c.Gender,
}).ToList(),
}).ToList(),
Children = allRegions
.Where(r => r.ParentUuid == region.Uuid)
@@ -216,25 +254,25 @@ public class regionController: ApiController
{
var allRegions = _db.Region.ToList();
// 根
// 根
var rootRegions = allRegions
.Where(r => r.ParentUuid == null)
.OrderBy(r => r.SortOrder)
.ToList();
// 生成树并按性别过滤
// 生成樹並按性別過濾
var tree = rootRegions
.Select(r => BuildRegionDtoByGender(r, allRegions, request.IsMale))
.Where(r => r != null) // 去掉有房间的区
.Where(r => r != null) // 去掉有房間的區
.ToList();
return Ok(tree);
}
// 根据性别过滤房间的 BuildRegionDto
// 根據性別過濾房間的 BuildRegionDto
private RegionDto BuildRegionDtoByGender(Region region, List<Region> allRegions, bool? gender)
{
// 过滤房间按性
// 過濾房間按性
var rooms = region.Room?
.Where(a => !gender.HasValue || a.Gender == gender.Value)
.Select(a => new RoomDto
@@ -251,19 +289,19 @@ public class regionController: ApiController
name = c.Name,
roomUuid = c.RoomUuid,
isactive = c.IsActive,
statusuuid = c.StatusUuid
statuscode = c.StatusCode,
}).ToList()
})
.ToList();
// 递归生成子
// 遞迴生成子
var children = allRegions
.Where(r => r.ParentUuid == region.Uuid)
.Select(child => BuildRegionDtoByGender(child, allRegions, gender))
.Where(c => c != null) // 去掉有房的子
.Where(c => c != null) // 去掉有房的子
.ToList();
// 如果这个区域既有房间也没有子域,返回 null
// 如果這個區域既有房間也沒有子域,返回 null
if (!rooms.Any() && !children.Any())
return null;
@@ -281,14 +319,14 @@ public class regionController: ApiController
BedDto = new List<BedDto>(),
Children = children,
Gender = region.Gender,
};
}
// 求模型
// 求模型
public class GenderRequest
{
public bool? IsMale { get; set; } // true = 男, false = 女, null = 不过滤
public bool? IsMale { get; set; } // true = 男, false = 女, null = 不過濾
}
public class RoomDto
@@ -303,11 +341,12 @@ public class regionController: ApiController
public Nullable<System.DateTime> UpdatedAt { get; set; }
public List<BedDto> beds { get; set; }
}
public class BedDto {
public class BedDto
{
public Guid Uuid { get; set; }
public Guid? roomUuid { get; set; }
public string name { get; set; }
public Guid? statusuuid { get; set; }
public string statuscode { get; set; }
public bool isactive { get; set; }
public bool Gender { get; set; }
}
@@ -324,13 +363,13 @@ public class regionController: ApiController
public bool IsActive { get; set; } = true;
public int? RoomCount { get; set; }
public List<RoomDto> Rooms { get; set; }
public List<BedDto> BedDto { get; set; }
public bool? Gender { get; set; }
public List<BedDto> BedDto { get; set; }
public bool? Gender { get; set; }
}
[HttpPost]
[Route("api/region/create")]
public IHttpActionResult createRegion([FromBody] RegionDto dto)
public IHttpActionResult createRegion([FromBody] RegionDto dto)
{
if (string.IsNullOrWhiteSpace(dto.Name))
return BadRequest("區域名稱為必填");
@@ -361,6 +400,28 @@ public class regionController: ApiController
var region = _db.Region.FirstOrDefault(r => r.Uuid == dto.Uuid);
if (region == null)
return NotFound();
if (dto.RoomCount < region.Room.Count())
{
return BadRequest("客房數量小於已存在的客房數量");
}
if (dto.IsActive == false)
{
var regionIds = GetAllRegionIds(region.Uuid);
var hasPendingBeds = _db.RegionRoomBed
.Where(b => regionIds.Contains(b.Room.RegionUuid))
.SelectMany(b => b.GuaDanOrderGuest)
.Any(g => !g.IsDeleted &&
(g.RegionRoomBedStatus.Code == GuaDanOrderGuest.STATUS_BOOKED || // 預約中
g.RegionRoomBedStatus.Code == GuaDanOrderGuest.STATUS_CHECKED_IN)); // 已入住
if (hasPendingBeds)
{
return Content(HttpStatusCode.BadRequest, new
{
code = "BED_IS_USED",
message = "該區域有床位正在掛單中,請先處理"
});
}
}
region.Name = dto.Name;
region.Description = dto.Description;
region.SortOrder = dto.SortOrder;
@@ -399,6 +460,25 @@ public class regionController: ApiController
return Ok(new { message = "刪除成功" });
}
public List<Guid> GetAllRegionIds(Guid regionUuid)
{
var regionIds = new List<Guid> { regionUuid };
var children = _db.Region
.Where(r => r.ParentUuid == regionUuid)
.Select(r => r.Uuid)
.ToList();
foreach (var childId in children)
{
regionIds.AddRange(GetAllRegionIds(childId));
}
return regionIds;
}
// 遞迴刪除子節點
private void DeleteRegionRecursive(Region region)
{
@@ -412,7 +492,7 @@ public class regionController: ApiController
[HttpPost]
[Route("api/region/getRegionType")]
public IHttpActionResult getRegionType()
{
{
var data = _db.RegionType.Where(a => a.IsActive == true).ToList();
return Ok(data);
}
@@ -421,7 +501,7 @@ public class regionController: ApiController
[Route("api/region/regionwithroom")]
public IHttpActionResult GetRegionWithRoom()
{
//返回有房的region
//返回有房的region
var data = _db.Region.Where(a => a.Room.Count() > 0)
.Select(r => new
{
@@ -431,7 +511,9 @@ public class regionController: ApiController
r.Gender,
rooms = r.Room.Select(room => new
{
room.Uuid, room.Name, room.RegionUuid
room.Uuid,
room.Name,
room.RegionUuid
}).ToList()
}).ToList();
return Ok(data);
@@ -440,7 +522,7 @@ public class regionController: ApiController
[Route("api/room/roomwithbed")]
public IHttpActionResult GetRoomWithBed(Guid? RegionUuid = null)
{
//取所有有床位的房
//取所有有床位的房
var query = _db.Room
.Select(r => new
{

View File

@@ -8,6 +8,7 @@ using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
@@ -40,24 +41,23 @@ public class regionRoomBedController : ApiController
.Where(a => a.RoomUuid == roomUuid)
.ToList();
// 取出所有相关排程
var schedules = _db.RegionAndRoomAndBedSchedule
.Where(b => b.IsDeleted == false && b.IsActive)
.ToList();
StartTime = StartTime.Date;
EndTime = EndTime?.Date;
var data = beds.Select(a =>
{
// 在内存中处理日期比较
var bedSchedules = schedules
var bedSchedules = _db.RegionAndRoomAndBedSchedule
.Where(s => s.GuaDanOrderGuest.StatusCode != "403")
.Where(s => s.GuaDanOrderGuest.StatusCode != "404")
.Where(b => b.TargetUuid == a.Uuid
&& (b.ScheduleDate == null // 长期占用
|| (b.ScheduleDate >= StartTime.Date && b.ScheduleDate <= EndTime.Value.Date)))
&& (b.ScheduleDate == null
|| (b.ScheduleDate >= StartTime && b.ScheduleDate < EndTime)))
.ToList()
.Select(c => new
{
c.Uuid,
c.Description,
c.IsDeleted,
c.IsActive,
c.GuaDanOrderNo,
c.UseType,
c.Title,
@@ -67,22 +67,51 @@ public class regionRoomBedController : ApiController
.ToList();
bool canUsed = !bedSchedules.Any();
bool bedIsStop = IsBedStopped(a);
return new
{
a.Uuid,
a.Name,
a.Gender,
a.IsActive,
a.StatusUuid,
a.StatusCode,
a.RoomUuid,
canUsed,
bedIsStop,
schedule = bedSchedules
};
});
return Ok(data);
}
public bool IsBedStopped(RegionRoomBed bed)
{
// 1⃣ 床位本身不可用
if (!bed.IsActive || bed.IsDeleted)
return true;
// 2⃣ 所属房间不可用
var room = bed.Room;
if (room == null || !room.IsActive.Value || room.IsDeleted)
return true;
// 3⃣ 所属区域不可用
var region = room.Region;
while (region != null)
{
if (!region.IsActive || region.IsDeleted)
return true; // 有任意一级区域不可用就返回 true
if (!region.ParentUuid.HasValue)
break; // 到顶层了
region = _db.Region.FirstOrDefault(r => r.Uuid == region.ParentUuid.Value);
}
// 4⃣ 全部检查通过 → 床位可用
return false;
}
[HttpPost]
[Route("api/region/bed/create")]
@@ -93,7 +122,7 @@ public class regionRoomBedController : ApiController
{
return BadRequest("當前客房不存在");
}
if(room.Gender != bed.Gender)
if (room.Gender != bed.Gender)
{
return BadRequest("床為性別和房間性別必須一致");
}
@@ -105,7 +134,7 @@ public class regionRoomBedController : ApiController
{
Name = bed.Name,
RoomUuid = bed.RoomUuid,
StatusUuid = bed.StatusUuid,
StatusCode = bed.StatusCode,
IsActive = bed.IsActive,
Gender = bed.Gender,
Uuid = Guid.NewGuid(),
@@ -114,10 +143,11 @@ public class regionRoomBedController : ApiController
_db.SaveChanges();
//創建床位
return Ok(new {
return Ok(new
{
uuid = regionBed.Uuid,
roomUuid = regionBed.RoomUuid,
statusuuid = regionBed.StatusUuid,
statuscode = regionBed.StatusCode,
isactive = regionBed.IsActive,
gender = regionBed.Gender,
name = regionBed.Name,
@@ -146,7 +176,21 @@ public class regionRoomBedController : ApiController
{
return BadRequest("床為性別和房間性別必須一致");
}
oldBed.StatusUuid = bed.StatusUuid;
if (bed.IsActive == false)
{
var hasPendingBeds = oldBed.GuaDanOrderGuest.Any(g => !g.IsDeleted &&
(g.RegionRoomBedStatus.Code == GuaDanOrderGuest.STATUS_BOOKED || // 预约中
g.RegionRoomBedStatus.Code == GuaDanOrderGuest.STATUS_CHECKED_IN));
if (hasPendingBeds)
{
return Content(HttpStatusCode.BadRequest, new
{
code = "BED_IS_USED",
message = "該床位正在掛單中,請先處理"
});
}
}
oldBed.StatusCode = bed.StatusCode;
oldBed.IsActive = bed.IsActive;
oldBed.Name = bed.Name;
oldBed.Gender = bed.Gender;
@@ -157,25 +201,75 @@ public class regionRoomBedController : ApiController
}
[HttpPost]
[Route("api/region/bed/delete")]
public IHttpActionResult delete([FromUri] Guid uuid)
public IHttpActionResult Delete([FromUri] Guid uuid)
{
var bed = _db.RegionRoomBed.Find(uuid);
if (bed == null)
{
return BadRequest("未找到床位");
}
_db.RegionRoomBed.Remove(bed);
_db.SaveChanges();
return Ok(new { message = "刪除成功" });
try
{
_db.RegionRoomBed.Remove(bed);
_db.SaveChanges();
return Ok(new { message = "刪除成功" });
}
catch (System.Data.Entity.Infrastructure.DbUpdateException ex)
{
// 判斷是否為外鍵關聯錯誤
if (ex.InnerException?.InnerException is System.Data.SqlClient.SqlException sqlEx &&
(sqlEx.Number == 547)) // 547 = SQL Server 外鍵違反錯誤碼
{
return BadRequest("刪除失敗:該床位已被使用或存在關聯資料,無法刪除。");
}
return InternalServerError(ex); // 其他資料庫錯誤
}
catch (Exception ex)
{
return InternalServerError(ex); // 其他未預期錯誤
}
}
[HttpGet]
[Route("api/region/bed/getavailablebedcountbytime")]
public async Task<IHttpActionResult> GetCanUseBedCountByTime(DateTime startTime, DateTime endTime)
{
//获取某个时间段内可用床位数量
var counts = await RegionAndRoomAndBedSchedule.GetAvailableBedCountsAsync(_db, startTime, endTime);
return Ok(counts);
var start = startTime.Date;
var end = endTime.Date;
// 找出所有在日期範圍內被占用的床位
var busyBedUuids = await _db.RegionAndRoomAndBedSchedule
.Where(s => s.GuaDanOrderGuest.StatusCode != "403")
.Where(s => s.GuaDanOrderGuest.StatusCode != "404")
.Where(a => a.IsCancel == false)
.Where(s => s.IsDeleted == false
&& (s.ScheduleDate == null // 長期占用
|| (s.ScheduleDate >= start && s.ScheduleDate < end)))
.Select(s => s.TargetUuid)
.Distinct()
.ToListAsync();
// 可用床位 = 所有床位 - 忙碌床位
var availableBeds = _db.RegionRoomBed
.Where(b => b.IsActive)
.Where(b => !busyBedUuids.Contains(b.Uuid));
var result = await availableBeds
.GroupBy(b => b.Gender)
.Select(g => new
{
Gender = g.Key,
Count = g.Count()
})
.ToListAsync();
var male = result.Where(r => r.Gender == true).Select(r => r.Count).FirstOrDefault();
var female = result.Where(r => r.Gender == false).Select(r => r.Count).FirstOrDefault();
return Ok(new {male, female});
}
[HttpPost]
@@ -260,7 +354,7 @@ public class regionRoomBedController : ApiController
}
}
if(index < followers.Count)
if (index < followers.Count)
{
isAllallocation = false;
return;
@@ -313,7 +407,7 @@ public class regionRoomBedController : ApiController
{
// 先拉出床位相關排程到內存,避免 EF 不支援 .Date
var schedules = _db.RegionAndRoomAndBedSchedule
.Where(s => s.IsDeleted == false && s.IsActive && s.TargetUuid == req.bedUuid)
.Where(s => s.IsDeleted == false && !s.IsCancel && s.TargetUuid == req.bedUuid)
.ToList();
bool conflictExists = schedules.Any(s =>
@@ -349,13 +443,14 @@ public class regionRoomBedController : ApiController
RoomUuid = roomUuid,
CheckInAt = allocationStart,
CheckOutAt = allocationEnd,
StatusCode = "401",
};
_db.GuaDanOrderGuest.Add(guest);
// 新增每日排程
if (allocationEnd.HasValue)
{
for (var date = allocationStart; date <= allocationEnd.Value; date = date.AddDays(1))
for (var date = allocationStart; date < allocationEnd.Value; date = date.AddDays(1))
{
var newSchedule = new RegionAndRoomAndBedSchedule
{
@@ -364,10 +459,11 @@ public class regionRoomBedController : ApiController
ScheduleDate = date,
UseType = (int)RegionAndRoomAndBedSchedule.SchedulePurpose.Bed_Reservation,
IsDeleted = false,
IsActive = true,
CreatedBy = "系统自动分配",
CreatedAt = DateTime.Now,
GuaDanOrderNo = guest.GuaDanOrderNo
GuaDanOrderNo = guest.GuaDanOrderNo,
Title = "掛單",
GuaDanOrderGuestUuid = guest.Uuid,
};
_db.RegionAndRoomAndBedSchedule.Add(newSchedule);
}
@@ -382,10 +478,11 @@ public class regionRoomBedController : ApiController
ScheduleDate = null,
UseType = (int)RegionAndRoomAndBedSchedule.SchedulePurpose.Bed_Reservation,
IsDeleted = false,
IsActive = true,
CreatedBy = "系统自动分配",
CreatedAt = DateTime.Now,
GuaDanOrderNo = guest.GuaDanOrderNo
GuaDanOrderNo = guest.GuaDanOrderNo,
Title = "掛單",
GuaDanOrderGuestUuid = guest.Uuid,
};
_db.RegionAndRoomAndBedSchedule.Add(newSchedule);
}
@@ -406,7 +503,7 @@ public class regionRoomBedController : ApiController
private bool BedIsCanUsed(RegionRoomBed bed, DateTime? StartTime, DateTime? EndTime)
{
if(!bed.IsActive)
if (!bed.IsActive)
{
return false;
}

View File

@@ -9,7 +9,7 @@ using System.Web.Http;
/// regionRoomBedStatusController 的摘要描述
/// </summary>
[ezAuthorize]
public class regionRoomBedStatusController: ApiController
public class regionRoomBedStatusController : ApiController
{
private Model.ezEntities _db = new Model.ezEntities();
public regionRoomBedStatusController()
@@ -34,7 +34,6 @@ public class regionRoomBedStatusController: ApiController
s.Code,
s.Name,
s.Description,
s.Uuid,
s.CategoryName
})
.ToList();
@@ -53,9 +52,9 @@ public class regionRoomBedStatusController: ApiController
}
[HttpPost]
[Route("api/region/bed/status/delete")]
public IHttpActionResult DeleteBedStatus([FromUri]Guid id)
public IHttpActionResult DeleteBedStatus([FromUri] string code)
{
var rt = _db.RegionRoomBedStatus.Find(id);
var rt = _db.RegionRoomBedStatus.Find(code);
if (rt == null)
{
return NotFound();

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Data.Entity;
using System.Drawing;
using System.Linq;
using System.Net;
using System.ServiceModel.Channels;
using System.Threading.Tasks;
using System.Web;
@@ -14,7 +15,7 @@ using static regionController;
/// regionRoomController 的摘要描述
/// </summary>
[ezAuthorize]
public class regionRoomController: ApiController
public class regionRoomController : ApiController
{
private Model.ezEntities _db = new Model.ezEntities();
public regionRoomController()
@@ -38,6 +39,15 @@ public class regionRoomController: ApiController
{
return BadRequest("請輸入床位數量");
}
var region = _db.Region.Find(room.RegionUuid);
if(region == null)
{
return BadRequest("未找到客房所屬的區域");
}
if(region.Room.Count() >= region.RoomCount)
{
return BadRequest("當前區域客房數量已經達到上限");
}
var newRoom = new Room();
newRoom.Name = room.Name;
newRoom.RegionUuid = room.RegionUuid;
@@ -62,7 +72,7 @@ public class regionRoomController: ApiController
name = c.Name,
roomUuid = c.RoomUuid,
isactive = c.IsActive,
statusuuid = c.StatusUuid,
statuscode = c.StatusCode,
}).ToList(),
};
@@ -86,10 +96,27 @@ public class regionRoomController: ApiController
// 如果有不符合性別的床位,不能繼續操作
return BadRequest("房間中已有與房間性別不符的床位,無法操作");
}
if(!room.BedCount.HasValue)
if (!room.BedCount.HasValue)
{
return BadRequest("請輸入床位數量");
}
if (room.IsActive == false)
{
var hasPendingBeds = oldRoom.RegionRoomBed
.SelectMany(b => b.GuaDanOrderGuest)
.Any(g => !g.IsDeleted &&
(g.RegionRoomBedStatus.Code == GuaDanOrderGuest.STATUS_BOOKED || // 预约中
g.RegionRoomBedStatus.Code == GuaDanOrderGuest.STATUS_CHECKED_IN)); // 已入住
if (hasPendingBeds)
{
return Content(HttpStatusCode.BadRequest, new
{
code = "BED_IS_USED",
message = "該房间有床位正在掛單中,請先處理"
});
}
}
oldRoom.Name = room.Name;
oldRoom.BedCount = room.BedCount;
oldRoom.Gender = room.Gender;
@@ -97,21 +124,49 @@ public class regionRoomController: ApiController
oldRoom.IsActive = room.IsActive;
oldRoom.RegionUuid = room.RegionUuid;
await _db.SaveChangesAsync();
return Ok(new { message = "更新成功"});
return Ok(new { message = "更新成功" });
}
[HttpPost]
[Route("api/region/room/delete")]
public async Task<IHttpActionResult> deleteRoom([FromBody] Room rm)
{
var room = await _db.Room.FindAsync(rm.Uuid);
if (room == null) return BadRequest("房間不存在");
using (var transaction = _db.Database.BeginTransaction())
{
try
{
var room = await _db.Room.FindAsync(rm.Uuid);
if (room == null) return BadRequest("房間不存在");
var beds = _db.RegionRoomBed.Where(b => b.RoomUuid == room.Uuid);
_db.RegionRoomBed.RemoveRange(beds);
var beds = _db.RegionRoomBed.Where(b => b.RoomUuid == room.Uuid);
_db.RegionRoomBed.RemoveRange(beds);
_db.Room.Remove(room);
await _db.SaveChangesAsync();
return Ok(new { message = "刪除成功" });
_db.Room.Remove(room);
await _db.SaveChangesAsync();
transaction.Commit();
return Ok(new { message = "刪除成功" });
}
catch (System.Data.Entity.Infrastructure.DbUpdateException ex)
{
transaction.Rollback();
// 判断是否为外键约束错误
if (ex.InnerException?.InnerException is System.Data.SqlClient.SqlException sqlEx &&
sqlEx.Number == 547) // 547 = 外键冲突
{
return BadRequest("房間或床位正在被使用,不能刪除");
}
return InternalServerError(ex);
}
catch (Exception ex)
{
transaction.Rollback();
return InternalServerError(ex);
}
}
}
}

View File

@@ -28,7 +28,7 @@
:server-items-length="activity_statistics.totalItems"
:items="activity_statistics.items">
<template #item.detail_btn="{item}">
<a :href="'activity.aspx?num='+item.id" class="btn btn-outline-secondary btn-sm" target="_blank">詳細統計</a>
<a :href="'statistics.aspx?num='+item.id + '&activity_name=' + encodeURIComponent(item.activity_name)" class="btn btn-outline-secondary btn-sm" target="_blank">詳細統計</a>
</template>
<template #item.duetime="{item}">
{{item.startdate|timeString("YYYY/MM/DD")}}-{{item.enddate|timeString("YYYY/MM/DD")}}

View File

@@ -0,0 +1,186 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="statistics.aspx.cs" Inherits="admin_activity_statistics_statistics" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" Runat="Server">
<div class="ms-5">
<span>
活動名稱: {{ activity_name }}
</span>
</div>
<div class="me-5">
<button class="btn btn-primary" type="button">匯出表格資料</button>
</div>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<div id="content" class="container-fluid">
<div class="card shadow-sm my-2" id="sec2">
<div class="card-header py-0">
<nav class="navbar py-0">
<div class="nav nav-tabs">
<button class="nav-link active" id="sec2-tab1" data-bs-toggle="tab" data-bs-target="#sec2-page1"
type="button" role="tab" aria-controls="home" aria-selected="true">
活動統計總表</button>
<button class="nav-link" id="sec2-tab2" data-bs-toggle="tab" data-bs-target="#sec2-page2"
type="button" role="tab" aria-controls="profile" aria-selected="false">
活動統計數量明細 </button>
<button class="nav-link" id="sec2-tab3" data-bs-toggle="tab" data-bs-target="#sec2-page3"
type="button" role="tab" aria-controls="profile" aria-selected="false">
活動統計報名人明細 </button>
<button class="nav-link" id="sec2-tab4" data-bs-toggle="tab" data-bs-target="#sec2-page4"
type="button" role="tab" aria-controls="profile" aria-selected="false">
活動牌位明細</button>
</div>
</nav>
</div>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active noedit p-4" id="sec2-page1" role="tabpanel" aria-labelledby="sec2-tab1">
<h5 class="mb-4"> {{ activity_name }}(截至 {{summaryStats?.reportDate }}</h5>
<!-- 基本資訊 + 功德金 統計 -->
<table class="table table-bordered table-sm w-auto mb-4">
<tbody>
<!-- 基本資訊 -->
<tr class="table-primary">
<th colspan="4">基本資訊</th>
</tr>
<tr>
<th>類別</th>
<td>報名人數</td>
<td>男眾人數</td>
<td>女眾人數</td>
</tr>
<tr>
<th>數量</th>
<td>{{ summaryStats?.totalApplicants }}</td>
<td>{{ summaryStats?.maleApplicants }}</td>
<td>{{ summaryStats?.femaleApplicants }}</td>
</tr>
</tbody>
</table>
<table class="table table-bordered table-sm w-auto mb-4">
<tbody>
<!-- 功德金 -->
<tr class="table-primary">
<th colspan="4">功德金統計</th>
</tr>
<tr>
<th>類別</th>
<td>總功德金</td>
<td>已收</td>
<td>未收</td>
</tr>
<tr>
<th>金額</th>
<td>{{ summaryStats?.donation.total }}</td>
<td>{{ summaryStats?.donation.received }}</td>
<td>{{ summaryStats?.donation.unreceived }}</td>
</tr>
</tbody>
</table>
<!-- 功德項目 -->
<table class="table table-bordered table-sm w-auto mb-4">
<tbody>
<tr class="table-primary">
<th colspan="100%">功德項目</th>
</tr>
<tr>
<th>功德項目</th>
<td v-for="(item, index) in summaryStats?.items" :key="'name-' + index">
{{ item.name }}
</td>
</tr>
<tr>
<th>人數</th>
<td v-for="(item, index) in summaryStats?.items" :key="'count-' + index">
{{ item.count }}
</td>
</tr>
</tbody>
</table>
<!-- 牌位數量 -->
<table class="table table-bordered table-sm w-auto">
<tbody>
<tr class="table-primary">
<th colspan="100%">牌位數量</th>
</tr>
<tr>
<th>牌位名稱</th>
<td v-for="(plaque, index) in summaryStats?.plaques" :key="'plaque-name-' + index">
{{ plaque.name }}
</td>
</tr>
<tr>
<th>數量</th>
<td v-for="(plaque, index) in summaryStats?.plaques" :key="'plaque-count-' + index">
{{ plaque.count }}
</td>
</tr>
</tbody>
</table>
</div>
<div class="tab-pane fade" id="sec2-page2" role="tabpanel" aria-labelledby="sec2-tab2">
sec2-page2
</div>
<div class="tab-pane fade" id="sec2-page3" role="tabpanel" aria-labelledby="sec2-tab3">
sec2-page3
</div>
<div class="tab-pane fade" id="sec2-page4" role="tabpanel" aria-labelledby="sec2-tab4">
sec2-page4
</div>
</div>
</div>
</div>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" Runat="Server">
<script>
new Vue({
el: '#app',
vuetify: new Vuetify(vuetify_options),
data() {
return {
activity_num: '<%= Request["num"] %>',
activity_name: '<%= Request["activity_name"] %>',
summaryStats: null, //匯總統計(總覽)
quantityDetails: [], // 數量明細(分類數量)
applicantList: [], // 報名人清單
plaqueList: [], //牌位清單
loadingSummary: false,
}
},
methods: {
fetchSummaryStats() {
//獲取匯總明細
this.loadingSummary = true;
axios.get(HTTP_HOST + 'api/activity/statistics/summary',
{
params: {
activity_num: this.activity_num
}
}
)
.then(res => {
this.summaryStats = res.data;
})
.catch(err => {
console.error(err);
})
.finally(() => {
this.loadingSummary = false;
});
}
},
mounted() {
this.fetchSummaryStats(); // 頁面載入時就獲取
}
})
</script>
</asp:Content>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class admin_activity_statistics_statistics : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{
}
}

View File

@@ -0,0 +1,908 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="index.aspx.cs" Inherits="admin_ancestraltablet_ancestraltabletarea_index" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" Runat="Server">
<nav class="mb-2 ps-3">
<button class="btn btn-primary me-2" type="button" @click="showNewtAreadialogMethod">
<i class="mdi mdi-plus"></i> 新增區域
</button>
<button class="btn btn-secondary me-2" @click="expandAll" type="button">
<i class="mdi mdi-arrow-expand-all"></i> 全部展開
</button>
<button class="btn btn-secondary" @click="collapseAll" type="button">
<i class="mdi mdi-arrow-collapse-all"></i> 全部收起
</button>
</nav>
<nav>
<button type="button" class="btn btn-primary" @click="toggleAreaData">
{{ showAreaDataFlag ? '隱藏區域資料' : '顯示區域資料' }}
</button>
</nav>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<div class="container-fluid">
<div class="row">
<div class="col-sm-4 col-lg-3">
<div class="card shadow-sm my-2">
<div class="card-header">神主牌區域列表</div>
<div class="card-body">
<ul class="tree">
<li v-for="area in ancestral_tablet_areas" :key="area.AreaId">
<region-item
:item="area"
:selected-id="currentSelectAreaId"
@select-area="selectAreaMethod"
:expand-all="expandAllFlag"
:collapse-all="collapseAllFlag"
@clear-expand-all="expandAllFlag = false"
@clear-collapse-all="collapseAllFlag = false"
/>
</li>
</ul>
</div>
</div>
</div>
<div class="col-sm-4 col-lg-9" v-if="currentSelectArea && showAreaDataFlag">
<div class="card shadow-sm my-2"style="position: sticky; top: 20px;">
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
<div>
<span class="fw-bold">
{{ ' ' + currentSelectArea?.areaName + ' ' }}
</span>
<span>
資料
</span>
</div>
<div>
<button class="btn btn-primary" type="button" @click="showEidtAreadialogMethod">
編輯
</button>
</div>
</div>
<div class="card-body">
<div class="container">
<div class="row">
<div class="col-12 col-sm-6">
<label>區域名稱</label>
<input v-model="currentSelectArea.areaName" type="text" class="form-control" readonly/>
</div>
<div class="col-12 col-sm-6">
<label>區域編號</label>
<input v-model="currentSelectArea.areaCode" type="text" class="form-control" readonly/>
</div>
<div class="col-12 col-sm-6">
<label>上層區域(可選)</label>
<input v-model="currentSelectArea.parentAreaId" type="text" class="form-control" readonly />
</div>
<div class="col-12 col-sm-6">
<label>區域類型(可空)</label>
<input v-model="currentSelectArea.areaType" type="text" class="form-control" readonly/>
</div>
<div class="col-12 col-sm-6">
<label>價格</label>
<input v-model="currentSelectArea.price" class="form-control" readonly/>
</div>
<div class="col-12 col-sm-6">
<label>排序</label>
<input v-model="currentSelectArea.sortOrder" class="form-control" readonly/>
</div>
<div class="col-12 col-sm-6">
<div style="display: flex; align-items: center; height: 100%; margin-top: 8px;">
<input
v-model="currentSelectArea.isDisabled"
type="checkbox"
disabled
id="disabledToggle"
style="width: 20px; height: 20px; margin-right: 8px; cursor: pointer;"
/>
<label
for="disabledToggle"
style="font-weight: bold; font-size: 14px; color: #333; margin: 0;"
>
是否停用
</label>
</div>
</div>
<div class="col-12 mt-3">
<label>描述</label>
<textarea v-model="currentSelectArea.description" rows="3" class="form-control" readonly></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-4 col-lg-9" v-if="currentSelectArea">
<div class="card shadow-sm my-2" style="flex: 1 1 auto; min-height: 0; overflow: auto;">
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;background-color: #ffc107;">
<div>
{{currentSelectArea.areaName + ' - ' + '神主牌位置'}}
</div>
<div>
<v-btn color="primary" @click="openNewPositionDialogMethod">批次新增神主牌位置</v-btn>
</div>
</div>
<div class="card-body">
<div class="grid-container">
<div
v-for="pos in positions"
:key="pos.positionCode"
class="grid-item"
:class="'status-' + pos.statusCode"
:style="{ gridRow: pos.rowNo, gridColumn: pos.columnNo }"
>
<div class="position-name">{{ pos.positionName }}</div>
<div class="position-content">
<!-- 這裡可以放更多資訊,比如價格或狀態 -->
<!-- 例如:價格: {{ pos.price }} -->
<v-btn small @click="editPositionMethod(pos)">修改</v-btn>
<v-btn small @click="deletePositionMethod(pos)">刪除</v-btn>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 新增區域彈出視窗 -->
<div>
<v-dialog v-model="showNewAreadialogFlag" max-width="1200px">
<v-card
style="min-height: 50vh; max-height: 80vh; overflow-y: auto;"
>
<v-card-title>
<span class="headline">新增區域</span>
</v-card-title>
<v-card-text>
<div class="container">
<div class="row">
<div class="col-12 col-sm-6">
<label>區域名稱</label>
<input v-model="newArea.areaName" type="text" class="form-control" />
</div>
<div class="col-12 col-sm-6">
<label>區域編號</label>
<input v-model="newArea.areaCode" type="text" class="form-control" />
</div>
<div class="col-12 col-sm-6">
<label>上層區域(可選)</label>
<select v-model="newArea.parentAreaId" class="form-control" >
<option value="">請選擇</option>
<!-- 手動添加選項 -->
<option v-for="r in flatAreas"
:value="r.areaId"
:disabled="disabledParentOptions.includes(r.areaId)">{{ r.areaName }}
</option>
</select>
</div>
<div class="col-12 col-sm-6">
<label>區域類型(可空)</label>
<input type="text" class="form-control" />
</div>
<div class="col-12 col-sm-6">
<label>價格</label>
<input v-model="newArea.price" class="form-control" />
</div>
<div class="col-12 col-sm-6">
<label>排序</label>
<input v-model="newArea.sortOrder" class="form-control" />
</div>
<div class="col-12 col-sm-6">
<div style="display: flex; align-items: center; height: 100%; margin-top: 8px;">
<input
v-model="newArea.isDisabled"
type="checkbox"
style="width: 20px; height: 20px; margin-right: 8px; cursor: pointer;"
/>
<label
for="disabledToggle"
style="font-weight: bold; font-size: 14px; color: #333; margin: 0;"
>
是否停用
</label>
</div>
</div>
<div class="col-12 mt-3">
<label>描述</label>
<textarea v-model="newArea.description" rows="3" class="form-control" ></textarea>
</div>
</div>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="closeNewAreadialogMethod">取消</v-btn>
<v-btn color="primary" @click="createNewAreaMethod">確定新增</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
<!-- 編輯區域彈出視窗 -->
<div>
<v-dialog v-model="showEidtAreadialogFlag" max-width="1200px">
<v-card
style="min-height: 50vh; max-height: 80vh; overflow-y: auto;"
>
<v-card-title>
<span class="headline">編輯區域</span>
</v-card-title>
<v-card-text>
<div class="container">
<div class="row">
<div class="col-12 col-sm-6">
<label>區域名稱</label>
<input v-model="editArea.areaName" type="text" class="form-control" required />
</div>
<div class="col-12 col-sm-6">
<label>區域編號</label>
<input v-model="editArea.areaCode" type="text" class="form-control" required />
</div>
<div class="col-12 col-sm-6">
<label>上層區域(可選)</label>
<select v-model="editArea.parentAreaId" class="form-control" >
<option value="">請選擇</option>
<!-- 手動添加選項 -->
<option v-for="r in flatAreas"
:value="r.areaId"
:disabled="disabledParentOptions.includes(r.areaId)">{{ r.areaName }}
</option>
</select>
</div>
<div class="col-12 col-sm-6">
<label>區域類型(可空)</label>
<input v-model="editArea.areaType" type="text" class="form-control" />
</div>
<div class="col-12 col-sm-6">
<label>價格</label>
<input v-model="editArea.price" class="form-control" />
</div>
<div class="col-12 col-sm-6">
<label>排序</label>
<input v-model="editArea.sortOrder" class="form-control" />
</div>
<div class="col-12 col-sm-6">
<div style="display: flex; align-items: center; height: 100%; margin-top: 8px;">
<input
v-model="editArea.isDisabled"
type="checkbox"
id="disabledToggle1"
style="width: 20px; height: 20px; margin-right: 8px; cursor: pointer;"
/>
<label
for="disabledToggle"
style="font-weight: bold; font-size: 14px; color: #333; margin: 0;"
>
是否停用
</label>
</div>
</div>
<div class="col-12 mt-3">
<label>描述</label>
<textarea v-model="editArea.description" rows="3" class="form-control" ></textarea>
</div>
</div>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="closeEidtAreadialogMethod">取消</v-btn>
<v-btn color="primary" @click="editAreaMethod">送出修改</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
<!-- 批次新增神主牌彈出視窗 -->
<div>
<!-- 彈出視窗組件 -->
<v-dialog v-model="showNewPositionDialogFlag" max-width="800px">
<v-card>
<v-card-title>
批次新增神主牌位置
<v-spacer></v-spacer>
<v-btn icon @click="closeNewPositionDialogMethod">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<div class="card p-3 mt-4">
<div class="row mb-2">
<div class="col">
<label>起始行</label>
<input v-model.number="batchPositionForm.startRow" type="number" class="form-control" />
</div>
<div class="col">
<label>起始列</label>
<input v-model.number="batchPositionForm.startCol" type="number" class="form-control" />
</div>
<div class="col">
<label>行數</label>
<input v-model.number="batchPositionForm.rows" type="number" class="form-control" />
</div>
<div class="col">
<label>列數</label>
<input v-model.number="batchPositionForm.cols" type="number" class="form-control" />
</div>
</div>
<div class="row mb-2">
<div class="col">
<label>價格</label>
<input v-model.number="batchPositionForm.price" type="number" class="form-control" />
</div>
<div class="col">
<label>狀態</label>
<select v-model="batchPositionForm.status" class="form-control">
<option v-for="s in statusList" :key="s.statusCode" :value="s.statusCode">
{{ s.statusName }}
</option>
</select>
</div>
<div class="col">
<label>Name模板</label>
<input v-model="batchPositionForm.nameTemplate" class="form-control" placeholder="如:神位編號{code}" />
</div>
</div>
<div class="row mb-3">
<div class="col">
<label>Code起始值</label>
<input v-model.number="batchPositionForm.startCode" type="number" class="form-control" />
</div>
<div class="col">
<label>Code長度</label>
<input v-model.number="batchPositionForm.codeLength" type="number" class="form-control" />
</div>
</div>
</div>
<div v-if="previewPositions.length">
<h3>預覽新增位置</h3>
<ul>
<li v-for="pos in previewPositions" :key="pos.PositionCode">
{{ pos.PositionCode }} - {{ pos.PositionName }} (行: {{ pos.RowNo }}, 列: {{ pos.ColumnNo }})
</li>
</ul>
</div>
</v-card-text>
<v-card-actions class="justify-end">
<v-btn color="primary" @click="generatePositionsMethod">生成預覽</v-btn>
<v-btn color="pirmary" @click="clearPreviewPositionsMethod">清除預覽</v-btn>
<v-btn color="primary" @click="confirmAddPositionsMethod">確認新增</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
<!-- 編輯牌位位置彈出視窗 -->
<div v-if="currentEditPosition">
<v-dialog v-model="editPositionFormVisible" max-width="500">
<v-card>
<v-card-title class="text-h6">
編輯位置:{{ currentEditPosition?.positionName || '未選擇' }}
</v-card-title>
<v-card-text>
<div class="form-row">
<label>位置名稱:</label>
<input v-model="currentEditPosition.positionName" class="form-control" />
</div>
<div class="form-row">
<label>價格:</label>
<input v-model="currentEditPosition.price" type="number" class="form-control" />
</div>
<div class="form-row">
<label>狀態:</label>
<select v-model="currentEditPosition.statusCode" class="form-control">
<option v-for="s in statusList" :key="s.statusCode" :value="s.statusCode">
{{ s.statusName }}
</option>
</select>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" @click="saveEditPositionMethod">保存</v-btn>
<v-btn text @click="editPositionFormVisible = false">取消</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" Runat="Server">
<script>
Vue.component('region-item', {
props: ['item', 'selectedId', 'expandAll', 'collapseAll'],
data() {
return {
expanded: false, // 預設全部收起
}
},
watch: {
expandAll(newVal) {
if (newVal) {
this.expanded = true;
// 執行完後發事件通知父組件清除標誌
this.$nextTick(() => this.$emit('clear-expand-all'));
}
},
collapseAll(newVal) {
if (newVal) {
this.expanded = false;
this.$nextTick(() => this.$emit('clear-collapse-all'));
}
}
},
computed: {
hasChildren() {
return this.item.children && this.item.children.length > 0;
},
icon() {
// 無論有無子節點,皆可點擊展開/收起
return this.expanded ? '▼' : '▶';
},
isSelected() {
return this.item.areaId === this.selectedId;
}
},
methods: {
toggle() {
this.expanded = !this.expanded;
},
select() {
this.$emit('select-area', this.item);
},
},
template: `
<div>
<span class="toggle-icon" @click="toggle">{{ icon }}</span>
<span @click="select"
class="region-item-label"
:class="{ 'selected': isSelected }">
{{ item.areaName }}
</span>
<!-- 子區域列表 -->
<ul v-if="hasChildren && expanded">
<li v-for="child in item.children" :key="child.areaId">
<region-item
:item="child"
:selected-id="selectedId"
:expand-all="expandAll"
:collapse-all="collapseAll"
@select-area="$emit('select-area', $event)"
@clear-expand-all="$emit('clear-expand-all')"
@clear-collapse-all="$emit('clear-collapse-all')"
/>
</li>
</ul>
</div>
`
});
new Vue({
el: '#app',
vuetify: new Vuetify(vuetify_options),
data() {
return {
expandAllFlag: false, // 控制全部展開
collapseAllFlag: false, // 控制全部收起
ancestral_tablet_areas: [], //神主牌區域列表
currentSelectArea: null,
currentSelectAreaId: null,
showEidtAreadialogFlag: false,
showNewAreadialogFlag: false,
showAreaDataFlag: false,//是否顯示區域資料
flatAreas: [],//所有區域展平
disabledParentOptions: [],//某個區域禁止選擇作為父區域的函數
newArea: {
areaId: null, // 自增主鍵,新增時通常為 null
areaName: null, // 區域名稱,必填
areaCode: null, // 區域編號,必填
parentAreaId: null, // 上層區域 ID可為 null
areaType: null, // 區域類型,可空
price: null, // 價格,可空
sortOrder: null, // 排序,可空
description: null, // 區域描述
isDisabled: null
},
editArea: {
areaId: null, // 自增主鍵,新增時通常為 null
areaName: null, // 區域名稱,必填
areaCode: null, // 區域編號,必填
parentAreaId: null, // 上層區域 ID可為 null
areaType: null, // 區域類型,可空
price: null, // 價格,可空
sortOrder: null, // 排序,可空
description: null, // 區域描述
isDisabled: null
},
statusList: [],
//--------------------------------神主牌位置變數
showNewPositionDialogFlag: false,//控制是否顯示批次新增神主牌彈出視窗
editPositionFormVisible: false, // 控制是否顯示編輯彈出視窗
currentEditPosition: null, // 儲存當前正在編輯的位置資訊
batchPositionForm: {
startRow: 1,
startCol: 1,
rows: 5,
cols: 10,
price: 0,
status: 'available',
nameTemplate: '神位{code}',
startCode: 1,
codeLength: 3
},
positions: [],
// 預覽數據
previewPositions: [],
//--------------------------------神主牌位置變數
}
},
methods: {
selectAreaMethod(area) {
this.currentSelectAreaId = area.areaId;
this.currentSelectArea = area;
const node = this.findRegionById(this.ancestral_tablet_areas, area.areaId);
this.disabledParentOptions = this.getAllDescendants(node);
this.loadTabletPositionsMethod(area.areaId);
},
showEidtAreadialogMethod() {
this.showEidtAreadialogFlag = true;
if (this.currentSelectArea) {
this.editArea = {
areaId: this.currentSelectArea?.areaId,
areaName: this.currentSelectArea?.areaName,
areaCode: this.currentSelectArea?.areaCode,
parentAreaId: this.currentSelectArea?.parentAreaId ?? null,
areaType: this.currentSelectArea?.areaType ?? null,
price: this.currentSelectArea?.price ?? null,
sortOrder: this.currentSelectArea?.sortOrder ?? null,
description: this.currentSelectArea?.description ?? null,
isDisabled: this.currentSelectArea?.isDisabled ?? true
};
}
},
closeEidtAreadialogMethod() {
this.showEidtAreadialogFlag = false;
this.resetEditArea();
},
showNewtAreadialogMethod() {
this.showNewAreadialogFlag = true;
},
closeNewAreadialogMethod() {
this.showNewAreadialogFlag = false;
this.resetNewArea();
},
createNewAreaMethod() {
//新建區域
axios.post(HTTP_HOST + 'api/ancestraltablet/area/create', this.newArea)
.then(response => {
this.closeEidtAreadialogMethod();
this.getAreaListMethod();
})
.catch(error => {
console.error('失敗:', error);
});
},
editAreaMethod() {
//修改區域資料
axios.post(HTTP_HOST + 'api/ancestraltablet/area/edit', this.editArea)
.then(response => {
this.currentSelectArea = response.data.area;
this.getAreaListMethod();
this.closeEidtAreadialogMethod();
})
.catch(error => {
console.error('失敗:', error);
});
},
resetNewArea() {
this.newArea = {
areaId: null,
areaName: null,
areaCode: null,
parentAreaId: null,
areaType: null,
price: null,
sortOrder: null,
description: null,
isDisabled: false
};
},
resetEditArea() {
this.editArea = {
areaId: null,
areaName: null,
areaCode: null,
parentAreaId: null,
areaType: null,
price: null,
sortOrder: null,
description: null,
isDisabled: false
};
},
getAreaListMethod() {
//獲取區域列表
axios.get(HTTP_HOST + 'api/ancestraltablet/area/getlist')
.then(res => {
this.ancestral_tablet_areas = res.data
this.flatAreas = this.flattenAreas(res.data);
})
},
expandAll() {
this.expandAllFlag = true;
this.collapseAllFlag = false;
},
collapseAll() {
this.collapseAllFlag = true;
this.expandAllFlag = false;
},
//區域展開是否可以被選擇作為上級區域相關函數
flattenAreas(data, list = []) {
data.forEach(item => {
list.push({ areaId: item.areaId, areaName: item.areaName });
if (item.children && item.children.length) {
this.flattenAreas(item.children, list);
}
});
return list;
},
findRegionById(list, areaId) {
for (const item of list) {
if (item.areaId === areaId) return item;
if (item.children) {
const found = this.findRegionById(item.children, areaId);
if (found) return found;
}
}
return null;
},
getAllDescendants(node) {
//尋找某個區域的所有子區域
const ids = [];
const dfs = (n) => {
ids.push(n.areaId);
if (n.children) {
n.children.forEach(child => dfs(child));
}
};
dfs(node);
return ids;
},
toggleAreaData() {
this.showAreaDataFlag = !this.showAreaDataFlag;
},
//--------------------------------神主牌位置相關函數
padCodeMethod(codeNum, length) {
return codeNum.toString().padStart(length, '0');
},
loadTabletPositionsMethod(areaId) {
axios.get(HTTP_HOST + 'api/ancestraltablet/area/position/getlist', {
params: {
areaId: areaId
}
})
.then(response => {
this.positions = response.data;
})
.catch(error => {
console.error('失敗:', error);
});
},
generatePositionsMethod() {
const form = this.batchPositionForm;
const positions = [];
let codeCounter = form.startCode;
for (let i = 0; i < form.rows; i++) {
for (let j = 0; j < form.cols; j++) {
const row = form.startRow + i;
const col = form.startCol + j;
const paddedCode = this.padCodeMethod(codeCounter, form.codeLength);
const positionCode = paddedCode;
const positionName = form.nameTemplate.replace('{code}', paddedCode);
positions.push({
AreaId: this.currentSelectArea.areaId,
RowNo: row,
ColumnNo: col,
PositionCode: positionCode,
PositionName: positionName,
Price: form.price,
StatusCode: form.status,
Description: ''
});
codeCounter++;
}
}
this.previewPositions = positions; // 先賦值預覽,不發請求
},
async confirmAddPositionsMethod() {
if (this.previewPositions.length === 0) {
alert('請先生成預覽數據');
return;
}
// 調用後端批次新增介面
try {
const response = await axios.post(
`${HTTP_HOST}api/ancestraltablet/position/batchcreate`,
this.previewPositions
);
// 如果後端成功響應HTTP 200/201
alert('批次新增成功');
this.loadTabletPositionsMethod(this.currentSelectArea.areaId); // 刷新數據
} catch (error) {
// 捕獲錯誤響應如500、400等
console.error('批次新增失敗', error);
let msg = '批次新增失敗';
if (error.response && error.response.data && error.response.data.exceptionMessage) {
msg += `${error.response.data.exceptionMessage}`;
}
alert(msg);
}
this.previewPositions = []; // 清空預覽
},
clearPreviewPositionsMethod() {
this.previewPositions = [];
},
openNewPositionDialogMethod() {
this.showNewPositionDialogFlag = true;
this.batchPositionForm.price = this.currentSelectArea.price
},
closeNewPositionDialogMethod() {
this.showNewPositionDialogFlag = false;
this.previewPositions = [];
},
editPositionMethod(position) {
// 彈出編輯表單、打開模態框或跳轉到編輯頁面
this.currentEditPosition = position;
this.editPositionFormVisible = true;
},
async saveEditPositionMethod() {
try {
await axios.post(`${HTTP_HOST}api/ancestraltablet/position/edit`, this.currentEditPosition);
this.$message?.success?.('保存成功') || alert('保存成功');
this.editPositionFormVisible = false;
this.loadTabletPositionsMethod(this.currentSelectArea.areaId);
this.currentEditPosition = null
} catch (error) {
console.error(error);
this.$message?.error?.('保存失敗') || alert('保存失敗');
}
},
async deletePositionMethod(position) {
if (confirm(`確定要刪除【${position.positionName}】嗎?`)) {
try {
await axios.delete(`${HTTP_HOST}api/ancestraltablet/position/delete/${position.positionId}`);
this.$message?.success?.('刪除成功'); // 如果用的是 Element Plus 或其他 UI 框架
this.loadTabletPositionsMethod(this.currentSelectArea.areaId); // 刷新數據
} catch (error) {
console.error('刪除失敗', error);
this.$message?.error?.('刪除失敗,請檢查網路或稍後再試');
}
}
},
//--------------------------------神主牌位置相關函數
async loadStatusList() {
//獲取狀態列表
try {
const response = await axios.get(`${HTTP_HOST}api/ancestraltablet/status/list`);
this.statusList = response.data;
} catch (err) {
console.error('獲取狀態列表失敗', err);
}
}
},
watch: {
},
mounted() {
this.getAreaListMethod();
this.loadStatusList();
}
});
</script>
<style>
.tree, .tree ul {
list-style: none;
margin: 0;
padding-left: 1rem;
}
.toggle-icon {
cursor: pointer;
user-select: none;
width: 1rem;
display: inline-block;
color: #007bff;
}
.region-item-label {
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
display: inline-block;
}
.region-item-label.selected {
background-color: #eaf4ff;
color: #0d6efd;
font-weight: bold;
}
.grid-container {
display: grid;
grid-template-columns: repeat(10, 120px); /* 6列 */
grid-auto-rows: 100px; /* 行高 */
gap: 10px;
}
/* 可用(綠色) */
.status-available {
background-color: #d4edda;
border-color: #28a745;
}
/* 維護中(黃色) */
.status-maintenance {
background-color: #fff3cd;
border-color: #ffc107;
}
/* 預訂中(藍色) */
.status-reserved {
background-color: #cce5ff;
border-color: #007bff;
}
/* 已使用(灰色) */
.status-used {
background-color: #e2e3e5;
border-color: #6c757d;
}
.grid-item {
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 1px 1px 3px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
}
.position-name {
background-color: #f0f0f0;
padding: 4px 8px;
font-weight: bold;
font-size: 14px;
text-align: center;
border-bottom: 1px solid #ddd;
}
.position-content {
flex-grow: 1;
padding: 3px;
font-size: 12px;
color: #666;
}
</style>
</asp:Content>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class admin_ancestraltablet_ancestraltabletarea_index : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{
}
}

View File

@@ -0,0 +1,569 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="index.aspx.cs" Inherits="admin_ancestraltablet_ancestraltabletposition_index" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" Runat="Server">
<nav class="mb-2 ps-3">
<button class="btn btn-secondary me-2" @click="expandAll" type="button">
<i class="mdi mdi-arrow-expand-all"></i> 全部展開
</button>
<button class="btn btn-secondary" @click="collapseAll" type="button">
<i class="mdi mdi-arrow-collapse-all"></i> 全部收起
</button>
</nav>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<div class="container-fluid">
<div class="row">
<div class="col-sm-4 col-lg-2">
<div class="card shadow-sm my-2">
<div class="card-header">神主牌區域列表</div>
<div class="card-body">
<ul class="tree">
<li v-for="area in ancestral_tablet_areas" :key="area.AreaId">
<region-item
:item="area"
:selected-id="currentSelectAreaId"
@select-area="selectAreaMethod"
:expand-all="expandAllFlag"
:collapse-all="collapseAllFlag"
@clear-expand-all="expandAllFlag = false"
@clear-collapse-all="collapseAllFlag = false"
/>
</li>
</ul>
</div>
</div>
</div>
<div class="col-sm-4 col-lg-10" v-if="currentSelectArea">
<div class="card shadow-sm my-2" style="position: sticky; top: 20px; display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0;">
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;background-color: #ffc107;">
<div>
{{currentSelectArea.areaName + ' - ' + '神主牌位置'}}
</div>
</div>
<div class="card-body" style="flex: 1 1 auto; min-height: 0; overflow: auto;">
<div class="grid-container">
<div
v-for="pos in positions"
:key="pos.positionCode"
class="grid-item"
:class="'status-' + pos.statusCode"
:style="{ gridRow: pos.rowNo, gridColumn: pos.columnNo }"
>
<div class="position-name">{{ pos.positionName }}</div>
<div class="position-content">
<span v-if="pos.statusCode == 'maintenance'">維護中</span>
<v-btn v-else-if="!pos.ancestralTabletRegistrant" @click="showCreatePWMethod(pos)">登記</v-btn>
<!-- 已預訂 -->
<div v-else>
<div>登記人:{{ pos.ancestralTabletRegistrant?.name }}</div>
<div>登記日期:{{ pos.ancestralTabletRegistrant?.registerDate|timeString('YYYY/MM/DD') }}</div>
<v-btn small color="btn-primary" @click="showEditPWMethod(pos)">
詳細資訊
</v-btn>
</>
</div>
<!-- 已使用 -->
<div v-else-if="pos.statusCode === 'used'">
<div>已使用</div>
<div v-if="pos.usedBy">使用人:{{ pos.usedBy }}</div>
<div v-if="pos.usedDate">使用日期:{{ pos.usedDate|timeString('YYYY/MM/DD') }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 登記資料彈出視窗 -->
<div>
<v-dialog v-model="showCreatePWFlag" max-width="800">
<v-card>
<v-card-title class="headline">登記資料({{'位置: ' + selectedPos?.positionName}})</v-card-title>
<v-card-text>
<!-- 登記人資料 -->
<hr />
<h5>登記人資料</h5>
<label>姓名:
<input class="form-control" type="text" v-model="form.createRegister.name" />
</label><br />
<label>電話:
<input class="form-control" type="text" v-model="form.createRegister.phone" />
</label><br />
<label>住址:
<input class="form-control" type="text" v-model="form.createRegister.address" />
</label><br />
<label>費用:
<input class="form-control" v-model="form.createRegister.price" />
</label><br />
<label>登記時間:
<input class="form-control" type="date" v-model="form.createRegister.registerDate" />
</label><br />
<label>開始時間:
<input class="form-control" type="date" v-model="form.createRegister.startDate" />
</label><br />
<label>結束時間:
<input class="form-control" type="date" v-model="form.createRegister.endDate" />
</label><br />
<label>長期有效:
<input type="checkbox" v-model="form.createRegister.isLongTerm" />
</label><br />
<label>是否啟用:
<input type="checkbox" v-model="form.createRegister.isActive" />
</label><br />
<div class="mt-2 mb-4">
<button
v-if="!form.createRegister.registrantCode"
class="btn btn-primary"
type="button"
@click="saveRegistrantMethod">
保存登記人
</button>
<button
v-else
class="btn btn-primary"
type="button"
@click="updateRegistrantMethod">
送出修改
</button>
</div>
<!-- 牌位資料 -->
<hr />
<h5>牌位資料</h5>
<label>牌位標題:
<input class="form-control" type="text" v-model="form.createPositionRecord.npTitle" />
</label><br />
<label>立牌時間:
<input class="form-control" type="date" v-model="form.createPositionRecord.npStandDate" />
</label><br />
<label>陽上:
<input class="form-control" type="text" v-model="form.createPositionRecord.npYangShang" />
</label><br />
<label>內牌內容:</label>
<textarea
class="form-control"
rows="4"
v-model="form.createPositionRecord.wpContent"
style="width: 100%; box-sizing: border-box;"
></textarea><br />
<div class="mt-2 mb-2">
<button v-if="form.createPositionRecord.recordId == null" class="btn btn-primary" type="button" @click="saveCreatePositionRecordMethod">保存牌位</button>
<button v-else type="button" class="btn btn-primary" @click="updateCreatePositionRecordMethod">
送出修改
</button>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<button class="btn btn-secondary" type="button" @click="closePWDialogMethod">關閉</button>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" Runat="Server">
<script>
Vue.component('region-item', {
props: ['item', 'selectedId', 'expandAll', 'collapseAll'],
data() {
return {
expanded: false, // 預設全部收起
}
},
watch: {
expandAll(newVal) {
if (newVal) {
this.expanded = true;
// 執行完後發事件通知父組件清除標誌
this.$nextTick(() => this.$emit('clear-expand-all'));
}
},
collapseAll(newVal) {
if (newVal) {
this.expanded = false;
this.$nextTick(() => this.$emit('clear-collapse-all'));
}
}
},
computed: {
hasChildren() {
return this.item.children && this.item.children.length > 0;
},
icon() {
// 無論有無子節點,皆可點擊展開/收起
return this.expanded ? '▼' : '▶';
},
isSelected() {
return this.item.areaId === this.selectedId;
}
},
methods: {
toggle() {
this.expanded = !this.expanded;
},
select() {
this.$emit('select-area', this.item);
},
},
template: `
<div>
<span class="toggle-icon" @click="toggle">{{ icon }}</span>
<span @click="select"
class="region-item-label"
:class="{ 'selected': isSelected }">
{{ item.areaName }}
</span>
<!-- 子區域列表 -->
<ul v-if="hasChildren && expanded">
<li v-for="child in item.children" :key="child.areaId">
<region-item
:item="child"
:selected-id="selectedId"
:expand-all="expandAll"
:collapse-all="collapseAll"
@select-area="$emit('select-area', $event)"
@clear-expand-all="$emit('clear-expand-all')"
@clear-collapse-all="$emit('clear-collapse-all')"
/>
</li>
</ul>
</div>
`
});
Vue.filter('timeString', function (value, myFormat) {
return value == null || value == "" ? "" : moment(value).format(myFormat || 'YYYY-MM-DD, HH:mm:ss');
});
new Vue({
el: '#app',
vuetify: new Vuetify(vuetify_options),
data() {
return {
expandAllFlag: false, // 控制全部展開
collapseAllFlag: false, // 控制全部收起
ancestral_tablet_areas: [], //神主牌區域列表
currentSelectArea: null,
currentSelectAreaId: null,
statusList: [],
//--------------------------------神主牌位置變數
positions: [],
selectedPos: null, //點擊登記的的pos
showCreatePWFlag: false, //控制是否打開登記神主牌位資料彈出視窗
form: {
createRegister: {
//新增登記人form
registrantCode: null, // 登記編號
positionId: null,
name: null,
phone: null,
address: null,
registerDate: null,
price: null,
startDate: null,
endDate: null,
isLongTerm: false,
isActive: true,
},
createPositionRecord: {
//新增牌位登記form
recordId: null, // 自增主鍵,前端一般不用填
registrantCode: null, // 外鍵,關聯登記人編號
npTitle: null,
npStandDate: null,
npYangShang: null,
wpContent: null,
},
},
//--------------------------------神主牌位置變數
}
},
methods: {
selectAreaMethod(area) {
this.currentSelectAreaId = area.areaId;
this.currentSelectArea = area;
this.loadTabletPositionsMethod(area.areaId);
},
getAreaListMethod() {
//獲取區域列表
axios.get(HTTP_HOST + 'api/ancestraltablet/area/getlist')
.then(res => {
this.ancestral_tablet_areas = res.data
})
},
expandAll() {
this.expandAllFlag = true;
this.collapseAllFlag = false;
},
collapseAll() {
this.collapseAllFlag = true;
this.expandAllFlag = false;
},
//--------------------------------神主牌位置相關函數
loadTabletPositionsMethod(areaId) {
axios.get(HTTP_HOST + 'api/ancestraltablet/position/getlist', {
params: {
areaId: areaId
}
})
.then(response => {
this.positions = response.data;
})
.catch(error => {
console.error('失敗:', error);
});
},
showCreatePWMethod(pos) {
//打開新增彈出視窗
this.selectedPos = pos;
this.showCreatePWFlag = true;
this.form.createRegister.positionId = pos.positionId
this.form.createRegister.price = pos.price
},
showEditPWMethod(pos) {
//打開編輯彈出視窗
this.selectedPos = pos;
const registrant = pos.ancestralTabletRegistrant;
if (registrant) {
this.form.createRegister.registrantCode = registrant.registrantCode;
this.form.createRegister.positionId = pos.positionId;
this.form.createRegister.name = registrant.name;
this.form.createRegister.phone = registrant.phone;
this.form.createRegister.address = registrant.address;
this.form.createRegister.registerDate = registrant.registerDate
? registrant.registerDate?.split('T')[0]
: '';
this.form.createRegister.price = registrant.price;
this.form.createRegister.startDate = registrant.startDate ? registrant.startDate?.split('T')[0]
: '';
this.form.createRegister.endDate = registrant.endDate ? registrant.endDate?.split('T')[0]
: '';
this.form.createRegister.isLongTerm = registrant.isLongTerm;
this.form.createRegister.isActive = registrant.isActive;
this.form.createPositionRecord.registrantCode = registrant.registrantCode;
this.showCreatePWFlag = true;
if (registrant.tabletRecord) {
this.form.createPositionRecord.recordId = registrant.tabletRecord.recordId;
this.form.createPositionRecord.registrantCode = registrant.tabletRecord.registrantCode;
this.form.createPositionRecord.npTitle = registrant.tabletRecord.npTitle;
this.form.createPositionRecord.npStandDate = registrant.tabletRecord.npStandDate ? registrant.tabletRecord.npStandDate.split('T')[0]
: '';
this.form.createPositionRecord.npYangShang = registrant.tabletRecord.npYangShang;
this.form.createPositionRecord.wpContent = registrant.tabletRecord.wpContent;
}
}
},
closePWDialogMethod(pos) {
//關閉編輯彈出視窗
this.selectedPos = null;
// 重設登記人欄位
this.form.createRegister = {
registrantCode: null,
positionId: null,
name: null,
phone: null,
address: null,
registerDate: null,
price: null,
startDate: null,
endDate: null,
isLongTerm: false,
isActive: false
};
// 清空牌位記錄
this.form.createPositionRecord = {
recordId: null,
registrantCode: null,
npTitle: null,
npStandDate: null,
npYangShang: null,
wpContent: null
};
this.showCreatePWFlag = false;
},
//--------------------------------神主牌位置相關函數
//--------------------------------登記人相關函數 start
async saveRegistrantMethod() {
try {
const response = await axios.post(HTTP_HOST + 'api/ancestraltablet/registrant/create', this.form.createRegister);
console.log('保存成功', response.data);
// 可選:提示用戶、關閉對話框、刷新數據等
this.$toast?.success('登記人保存成功'); // 取決於你是否使用 toast 插件
this.form.createRegister.registrantCode = response.data.registrantCode
this.form.createPositionRecord.registrantCode = response.data.registrantCode
this.loadTabletPositionsMethod(this.selectedPos.areaId)
alert("修改成功")
} catch (error) {
console.error('保存失敗', error);
this.$toast?.error('登記人保存失敗');
}
},
updateRegistrantMethod() {
axios.post(HTTP_HOST + 'api/ancestraltablet/registrant/update', this.form.createRegister)
.then(response => {
console.log('登記人更新成功:', response.data);
this.$toast?.success?.('登記人更新成功'); // 可選:使用 toast 彈出提示
//this.showCreatePWFlag = false; // 關閉彈出視窗
// 可選:刷新列表等
this.loadTabletPositionsMethod(this.selectedPos.areaId)
alert("修改成功")
})
.catch(error => {
console.error('更新登記人失敗:', error);
this.$toast?.error?.('更新失敗,請檢查數據');
});
},
//--------------------------------登記人相關函數 end
//--------------------------------牌位資料相關函數 Start
saveCreatePositionRecordMethod() {
// 校驗必須欄位
if (!this.form.createPositionRecord.registrantCode) {
alert('請先填寫登記人資料並且送出保存!');
return;
}
axios.post(HTTP_HOST + 'api/ancestraltablet/pw/create', this.form.createPositionRecord)
.then(res => {
if (res.data && res.data.message) {
alert(res.data.message);
} else {
alert('保存成功');
}
// 成功後可以關閉彈出視窗或清空表單
//this.showCreatePWFlag = false;
})
.catch(err => {
console.error('保存失敗:', err);
alert('保存失敗,請檢查伺服器日誌');
});
},
updateCreatePositionRecordMethod() {
// 校驗必須欄位
if (!this.form.createPositionRecord.recordId) {
alert('不存在牌位資料,無法更新');
return;
}
axios.post(HTTP_HOST + 'api/ancestraltablet/pw/update', this.form.createPositionRecord)
.then(res => {
if (res.data && res.data.message) {
alert(res.data.message);
} else {
alert('牌位資料更新成功');
}
// 成功後可以關閉彈出視窗或清空表單
//this.showCreatePWFlag = false;
})
.catch(err => {
alert('更新失敗:', err);
});
},
//--------------------------------牌位資料相關函數 end
async loadStatusList() {
//獲取狀態列表
try {
const response = await axios.get(`${HTTP_HOST}api/ancestraltablet/status/list`);
this.statusList = response.data;
} catch (err) {
console.error('獲取狀態列表失敗', err);
}
}
},
watch: {
},
mounted() {
this.getAreaListMethod();
this.loadStatusList();
}
});
</script>
<style>
.tree, .tree ul {
list-style: none;
margin: 0;
padding-left: 1rem;
}
.toggle-icon {
cursor: pointer;
user-select: none;
width: 1rem;
display: inline-block;
color: #007bff;
}
.region-item-label {
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
display: inline-block;
}
.region-item-label.selected {
background-color: #eaf4ff;
color: #0d6efd;
font-weight: bold;
}
.grid-container {
display: grid;
grid-template-columns: repeat(10, 150px); /* 6列 */
grid-auto-rows: 150px; /* 行高 */
gap: 10px;
}
/* 可用(綠色) */
.status-available {
background-color: #d4edda;
border-color: #28a745;
}
/* 維護中(黃色) */
.status-maintenance {
background-color: #fff3cd;
border-color: #ffc107;
}
/* 已使用(灰色) */
.status-used {
background-color: #e2e3e5;
border-color: #6c757d;
}
.grid-item {
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 1px 1px 3px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
}
.position-name {
background-color: #f0f0f0;
padding: 4px 8px;
font-weight: bold;
font-size: 14px;
text-align: center;
border-bottom: 1px solid #ddd;
}
.position-content {
flex-grow: 1;
padding: 3px;
font-size: 12px;
color: #666;
}
</style>
</asp:Content>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class admin_ancestraltablet_ancestraltabletposition_index : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{
}
}

View File

@@ -0,0 +1,126 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="index.aspx.cs" Inherits="admin_ancestraltablet_ancestraltabletstatistics_index" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" Runat="Server">
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<div class="container">
<h2 class="title">區域牌位統計</h2>
<table class="stats-table" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<th>區域編號</th>
<th>區域名稱</th>
<th>總位置數</th>
<th>可用位置數</th>
</tr>
</thead>
<tbody>
<tr v-for="area in areas" :key="area.areaId">
<td>{{ area.areaId }}</td>
<td>{{ area.areaName }}</td>
<td>{{ area.totalPositions }}</td>
<td>{{ area.availableCount }}</td>
</tr>
</tbody>
</table>
</div>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" Runat="Server">
<script>
new Vue({
el: '#app',
vuetify: new Vuetify(vuetify_options),
data() {
return {
areas: []
}
},
methods: {
getAncestralTabletPositionsStatisticsMethod() {
axios.get(HTTP_HOST + 'api/ancestraltablet/statistics/positions/availablepositions')
.then((res) => {
this.areas = res.data
})
.catch((error) => {
})
}
},
mounted() {
this.getAncestralTabletPositionsStatisticsMethod()
}
})
</script>
<style>
.container {
max-width: 800px;
margin: 50px auto;
background: #ffffff;
padding: 25px 30px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
font-family: "Microsoft YaHei", sans-serif;
}
.title {
text-align: center;
color: #2c3e50;
font-size: 22px;
font-weight: 600;
margin-bottom: 25px;
letter-spacing: 1px;
}
.stats-table {
width: 100%;
border-collapse: collapse;
font-size: 15px;
text-align: center;
color: #333;
}
.stats-table th {
background-color: #1976d2;
color: #fff;
padding: 12px;
font-weight: 600;
border: none;
}
.stats-table td {
padding: 10px;
border: 1px solid #e0e0e0;
}
/* 奇偶行區分 */
.stats-table tbody tr:nth-child(odd) {
background-color: #f8f9fa;
}
/* 滑鼠懸停高亮 */
.stats-table tbody tr:hover {
background-color: #e3f2fd;
transition: 0.3s ease;
}
/* 響應式支持 */
@media (max-width: 600px) {
.container {
padding: 15px;
}
.title {
font-size: 18px;
}
.stats-table th, .stats-table td {
padding: 8px;
font-size: 13px;
}
}
</style>
</asp:Content>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class admin_ancestraltablet_ancestraltabletstatistics_index : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{
}
}

View File

@@ -0,0 +1,15 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="create.aspx.cs" Inherits="admin_ancestraltablet_ancestraltabletuselist_create" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" Runat="Server">
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<div>
這是新增頁面
</div>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" Runat="Server">
</asp:Content>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class admin_ancestraltablet_ancestraltabletuselist_create : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{
}
}

View File

@@ -0,0 +1,99 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="detail.aspx.cs" Inherits="admin_ancestraltablet_ancestraltabletuselist_detail" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" Runat="Server">
<nav>
<a :href="'edit.aspx?registrantCode=' + registrantCode" class="btn btn-primary">修改資料</a>
</nav>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<div>
{{registrantCode}}
</div>
<div>
<div class="card">
<h2>登記人資訊</h2>
<p><strong>登記編碼:</strong> {{ registrant.registrantCode }}</p>
<p><strong>姓名:</strong> {{ registrant.name }}</p>
<p><strong>電話:</strong> {{ registrant.phone }}</p>
<p><strong>地址:</strong> {{ registrant.address }}</p>
<p><strong>登記日期:</strong> {{ formatDate(registrant.registerDate) }}</p>
<div style="border: 1px solid #007bff; background-color: #f0f8ff; padding: 10px 15px; border-radius: 5px; display: flex; flex-direction: column; gap: 5px; max-width: 400px; margin-bottom: 10px;">
<div>
<label style="font-weight: bold; margin-right: 5px;">已選擇位置:</label>
<span>{{ registrant?.positionName || '未選擇' }}</span>
</div>
</div>
<p><strong>價格:</strong> {{ registrant.price }}</p>
<p><strong>開始日期:</strong> {{ formatDate(registrant.startDate) }}</p>
<p><strong>結束日期:</strong> {{ formatDate(registrant.endDate) }}</p>
<p><strong>是否長期:</strong> {{ registrant.isLongTerm ? '是' : '否' }}</p>
<p><strong>是否啟用:</strong> {{ registrant.isActive ? '是' : '否' }}</p>
</div>
<div class="card" v-if="registrant.tabletRecord">
<h2>牌位資料</h2>
<p><strong>記錄ID:</strong> {{ registrant.tabletRecord.recordId }}</p>
<p><strong>登記編碼:</strong> {{ registrant.tabletRecord.registrantCode }}</p>
<p><strong>牌位標題:</strong> {{ registrant.tabletRecord.npTitle }}</p>
<p><strong>立牌日期:</strong> {{ formatDate(registrant.tabletRecord.npStandDate) }}</p>
<p><strong>陽上:</strong> {{ registrant.tabletRecord.npYangShang }}</p>
<p><strong>內牌內容:</strong> {{ registrant.tabletRecord.wpContent }}</p>
</div>
<div class="card" v-else>
<h2>牌位資料</h2>
<p>暫無牌位資料</p>
</div>
</div>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" Runat="Server">
<script>
new Vue({
el: '#app',
vuetify: new Vuetify(vuetify_options),
data() {
return {
registrantCode: '<%=Request.QueryString["registrantCode"]%>',
registrant: {}
}
},
methods: {
formatDate(dateStr) {
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleDateString();
},
getRegistrantByCodeMethod(code) {
axios.get(HTTP_HOST + 'api/ancestraltablet/registrant/getbycode', {
params: {
registrantCode: code
}
})
.then((res => {
this.registrant = res.data
}))
.catch((error => {
}))
}
},
mounted() {
this.getRegistrantByCodeMethod(this.registrantCode);
}
})
</script>
<style>
.card {
border: 1px solid #ccc;
padding: 15px;
margin-bottom: 20px;
border-radius: 5px;
background-color: #fafafa;
}
</style>
</asp:Content>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class admin_ancestraltablet_ancestraltabletuselist_detail : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{
}
}

View File

@@ -0,0 +1,419 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="edit.aspx.cs" Inherits="admin_ancestraltablet_ancestraltabletuselist_edit" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" Runat="Server">
<nav>
<a :href="'detail.aspx?registrantCode=' + registrantCode" class="btn btn-secondary">返回詳情</a>
</nav>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<div>
<!-- 登記人資料編輯區域 -->
<div class="card">
<h2>編輯登記人資訊</h2>
<label>登記編碼:</label>
<input type="text" v-model="registrant.registrantCode" class="form-control" disabled />
<label>姓名:</label>
<input type="text" v-model="registrant.name" class="form-control" />
<label>電話:</label>
<input type="text" v-model="registrant.phone" class="form-control" />
<label>地址:</label>
<input type="text" v-model="registrant.address" class="form-control" />
<label>登記日期:</label>
<input type="date" v-model="registrant.registerDate" class="form-control" />
<div class="mt-4" style="border: 1px solid #007bff; background-color: #f0f8ff; padding: 10px 15px; border-radius: 5px; display: flex; flex-direction: column; gap: 5px; max-width: 400px; margin-bottom: 10px;">
<div>
<label style="font-weight: bold; margin-right: 5px;">已選擇位置:</label>
<span>{{ registrant?.positionName || '未選擇' }}</span>
</div>
<div v-if="newPositionId">
<label style="font-weight: bold; margin-right: 5px;">新選擇位置:</label>
<span>{{ this.newPositionEntity?.positionName }}</span>
</div>
<button type="button" @click="isShowPositionDialog=true"
style="align-self: flex-start; padding: 5px 10px; background-color: #007bff; color: #fff; border: none; border-radius: 3px; cursor: pointer;"
>
{{registrant?.positionId ? '更換位置' : '選擇位置'}}
</button>
</div>
<label>價格:</label>
<input type="number" v-model="registrant.price" class="form-control" />
<label>開始日期:</label>
<input type="date" v-model="registrant.startDate" class="form-control" />
<label>結束日期:</label>
<input type="date" v-model="registrant.endDate" class="form-control" />
<div class="mt-3" style="display:flex; align-items:center; gap:15px; margin-bottom:10px; font-family:Arial, sans-serif;">
<label style="font-weight:500;">是否長期:</label>
<input type="checkbox" v-model="registrant.isLongTerm" style="width:16px; height:16px; cursor:pointer;" />
<span>是</span>
</div>
<div style="display:flex; align-items:center; gap:15px; margin-bottom:10px; font-family:Arial, sans-serif;">
<label style="font-weight:500;">是否啟用:</label>
<input type="checkbox" v-model="registrant.isActive" style="width:16px; height:16px; cursor:pointer;" />
<span>是</span>
</div>
<button type="button" class="btn btn-primary mt-2" @click="updateRegistrant">保存登記資料</button>
</div>
<!-- 牌位資料區域 -->
<div class="card" v-if="registrant.tabletRecord">
<h2>編輯牌位資料</h2>
<label>記錄ID:</label>
<input type="text" v-model="registrant.tabletRecord.recordId" class="form-control" disabled />
<label>登記編碼 (外鍵):</label>
<input type="text" v-model="registrant.tabletRecord.registrantCode" class="form-control" disabled />
<label>牌位標題:</label>
<input type="text" v-model="registrant.tabletRecord.npTitle" class="form-control" />
<label>立牌日期:</label>
<input type="date" v-model="registrant.tabletRecord.npStandDate" class="form-control" />
<label>陽上:</label>
<input type="text" v-model="registrant.tabletRecord.npYangShang" class="form-control" />
<label>內牌內容:</label>
<textarea v-model="registrant.tabletRecord.wpContent" class="form-control"></textarea>
<button type="button" class="btn btn-success mt-2" @click="updateTabletRecord">保存牌位資料</button>
</div>
<div class="card" v-else>
<h2>牌位資料</h2>
<p>暫無牌位資料</p>
<button type="button" class="btn btn-primary" @click="createTabletRecordForm">新增牌位資料</button>
<div v-if="creatingTablet" class="mt-3">
<label>牌位標題:</label>
<input type="text" v-model="newTablet.npTitle" class="form-control" />
<label>立牌日期:</label>
<input type="date" v-model="newTablet.npStandDate" class="form-control" />
<label>陽上:</label>
<input type="text" v-model="newTablet.npYangShang" class="form-control" />
<label>牌位內容:</label>
<textarea v-model="newTablet.wpContent" class="form-control"></textarea>
<button type="button" class="btn btn-success mt-2" @click="createTabletRecord">保存新增</button>
</div>
</div>
<div>
<v-dialog v-model="isShowPositionDialog" persistent
width="80%"
height="80%">
<v-card style="
width: 80vw;
height: 80vh;
display: flex;
flex-direction: column;
">
<v-card-title
style="display: flex; align-items: center; font-weight: 600; font-size: 18px;"
>
<span>選擇位置:</span>
<select
class="form-control"
style="
flex: 0 0 200px;
padding: 6px 10px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #fff;
font-size: 14px;
cursor: pointer;
outline: none;
transition: border-color 0.2s;
"
v-model="selectedArea"
@focus="e => e.target.style.borderColor = '#007bff'"
@blur="e => e.target.style.borderColor = '#ccc'"
@change="onAreaChange"
>
<option value="">請選擇區域</option>
<option v-for="area in areaList" :value="area.areaId" :key="area.areaId">{{area.areaName}}</option>
</select>
</v-card-title>
<v-card-text style="flex: 1; overflow: auto;">
<div class="grid-container">
<div
v-for="pos in positionList"
:key="pos.positionId"
class="grid-item"
:class="'status-' + (pos.isCanUse? 'canuse':'cannotuse')"
:style="{ gridRow: pos.rowNo, gridColumn: pos.columnNo }"
>
<div class="position-name">{{ pos.positionName }}</div>
<div class="position-content">
<button type="button" v-if="pos.isCanUse"
class="btn btn-primary"
@click="chooseNewPositionMethod(pos)"
>選擇</button>
<span v-else>已被使用</span>
</div>
</div>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<div class="me-5">新位置:{{newPositionEntity?.positionName}}</div>
<v-btn color="primary" @click="saveChoosePositionMethod">確定</v-btn>
<v-btn color="grey" @click="cancelChoosePositionMethod">取消</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</div>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" Runat="Server">
<script>
new Vue({
el: '#app',
vuetify: new Vuetify(vuetify_options),
data() {
return {
registrantCode: '<%=Request.QueryString["registrantCode"]%>',
registrant: {},
creatingTablet: false,
newPositionId: null,
newPositionEntity: null,
newTablet: {
//新增牌位登記form
recordId: null, // 自增主鍵,前端一般不用填
registrantCode: null, // 外鍵,關聯登記人編號
npTitle: null,
npStandDate: null,
npYangShang: null,
wpContent: null,
},
positionList: [],//選擇神位位置的時候獲取的位置列表
isShowPositionDialog: false,
selectedArea: "",
areaList: [],
}
},
methods: {
formatDate(dateStr) {
if (!dateStr) return "";
return dateStr.split('T')[0];
},
getRegistrantByCodeMethod(code) {
axios.get(HTTP_HOST + 'api/ancestraltablet/registrant/getbycode', {
params: {
registrantCode: code
}
})
.then((res => {
const data = res.data;
// 格式化登記日期欄位
if (data.registerDate) data.registerDate = this.formatDate(data.registerDate);
if (data.startDate) data.startDate = this.formatDate(data.startDate);
if (data.endDate) data.endDate = this.formatDate(data.endDate);
// 格式化牌位日期欄位
if (data.tabletRecord && data.tabletRecord.npStandDate)
data.tabletRecord.npStandDate = this.formatDate(data.tabletRecord.npStandDate);
this.registrant = data;
}))
.catch((error => {
}))
},
// 更新登記資料
updateRegistrant() {
const newPositionId = this.newPositionId; // 假設 newPositionId 存在組件裡
// 如果 newPositionId 不為空,則更新 registrant.PositionId
if (newPositionId != null && newPositionId !== '') {
this.registrant.PositionId = newPositionId;
}
axios.post(HTTP_HOST + 'api/ancestraltablet/registrant/update', this.registrant)
.then(() => {
alert('登記資料已保存!')
this.getRegistrantByCodeMethod(this.registrantCode);
this.newPositionId = null;
}
)
.catch(err => {
console.error(err);
alert('保存失敗!');
});
},
// 更新牌位資料
updateTabletRecord() {
axios.post(HTTP_HOST + 'api/ancestraltablet/pw/update', this.registrant.tabletRecord)
.then(() => alert('牌位資料已更新!'))
.catch(err => {
console.error(err);
alert('保存失敗!');
});
},
// 顯示新增牌位表單
createTabletRecordForm() {
this.creatingTablet = true;
},
// 新增牌位資料
createTabletRecord() {
const data = {
...this.newTablet,
registrantCode: this.registrant.registrantCode,
};
axios.post(HTTP_HOST + 'api/ancestraltablet/pw/create', data)
.then(() => {
alert('牌位資料已新增!');
this.creatingTablet = false;
this.getRegistrantByCodeMethod(this.registrantCode);
})
.catch(err => {
console.error(err);
alert('新增失敗!');
});
},
getPositionList(areaId) {
axios.get(HTTP_HOST + 'api/ancestraltablet/position/shortlist',
{
params: {
areaId: areaId
}
})
.then((res) => {
this.positionList = res.data
})
.catch((error) => {
});
},
getArea() {
axios.get(HTTP_HOST + 'api/ancestraltablet/area/getereawithposition')
.then((res) => {
this.areaList = res.data;
})
.catch();
},
onAreaChange() {
//獲取有神位的區域
if (!this.selectedArea) {
this.positionList = [];
return; // 如果沒有選擇,不請求
}
this.getPositionList(this.selectedArea)
},
chooseNewPositionMethod(newPos) {
this.newPositionEntity = newPos
},
closeChoosePositionDialogMethod() {
this.isShowPositionDialog = false;
this.selectedArea = "";
this.positionList = [];
},
cancelChoosePositionMethod() {
this.newPositionEntity = null;
this.closeChoosePositionDialogMethod()
},
saveChoosePositionMethod() {
this.newPositionId = this.newPositionEntity.positionId
this.closeChoosePositionDialogMethod()
},
},
mounted() {
this.getRegistrantByCodeMethod(this.registrantCode);
this.getArea()
}
})
</script>
<style>
.card {
border: 1px solid #ccc;
padding: 15px;
margin-bottom: 20px;
border-radius: 5px;
background-color: #fafafa;
width: 90%;
max-width: 900px;
min-width: 300px; /* 最小寬度防止太窄 */
margin: 0 auto; /* 居中 */
}
.grid-container {
display: grid;
grid-template-columns: repeat(10, 150px); /* 6列 */
grid-auto-rows: 150px; /* 行高 */
gap: 10px;
}
.status-available {
background-color: #d4edda;
border-color: #28a745;
}
/* 維護中(黃色) */
.status-maintenance {
background-color: #fff3cd;
border-color: #ffc107;
}
/* 已使用(灰色) */
.status-used {
background-color: #e2e3e5;
border-color: #6c757d;
}
/* 可以使用(綠色) */
.status-canuse {
background-color: #d4edda; /* 淺綠色背景 */
border-color: #28a745; /* 綠色邊框 */
color: #155724; /* 深綠色文字 */
}
/* 不能使用(紅色) */
.status-cannotuse {
background-color: #f8d7da; /* 淺紅色背景 */
border-color: #dc3545; /* 紅色邊框 */
color: #721c24; /* 深紅文字 */
}
.grid-item {
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 1px 1px 3px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
}
.position-name {
background-color: #f0f0f0;
padding: 4px 8px;
font-weight: bold;
font-size: 14px;
text-align: center;
border-bottom: 1px solid #ddd;
}
.position-content {
flex-grow: 1;
padding: 3px;
font-size: 12px;
color: #666;
}
</style>
</asp:Content>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class admin_ancestraltablet_ancestraltabletuselist_edit : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{
}
}

View File

@@ -0,0 +1,192 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="index.aspx.cs" Inherits="admin_ancestraltablet_ancestraltabletuselist_index" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" Runat="Server">
<nav>
<!--
<a :href="'create.aspx'" class="btn btn-primary">
新增使用申請
</a>
-->
<div style="display: inline">
<span>
查詢條件 :
</span>
<input v-model="search.registrantUserName" class="form-control" style="display:inline-block; width: auto;"
placeholder="請輸入登記人"
/>
<button type="button" class="btn btn-primary" @click="handleSearch">搜尋</button>
<button type="button" class="btn btn-primary" @click="clearSearch">清除條件</button>
</div>
</nav>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
神主牌使用記錄
<div>
<v-data-table
:items="registrantList"
:headers="headers"
hide-default-footer>
<template #item.registerdate="{item}">
{{item.registerDate |timeString('YYYY-MM-DD')}}
</template>
<template #item.startdate="{item}">
{{item.startDate |timeString('YYYY-MM-DD')}}
</template>
<template #item.enddate="{item}">
{{item.endDate |timeString('YYYY-MM-DD')}}
</template>
<template #item.createdat="{item}">
{{item.createdAt |timeString('YYYY-MM-DD HH:MM')}}
</template>
<template #item.islongterm="{item}">
{{item.isLongTerm ? "是": "否"}}
</template>
<template #item.isactive="{item}">
{{item.isActive ? "是" : "否"}}
</template>
<template #item.actions="{item}">
<a :href="'detail.aspx?registrantCode=' + item.registrantCode" class="btn btn-primary">詳細資訊</a>
</template>
</v-data-table>
<v-container>
<v-row class="align-baseline" wrap="false">
<v-col cols="12" md="8">
<v-pagination
v-model="options.page"
:length="pageCount">
</v-pagination>
</v-col>
<v-col class="text-truncate text-right" cols="12" md="2">
共 {{ total }} 筆, 頁數:
</v-col>
<v-col cols="6" md="1">
<v-text-field
v-model="options.page"
type="number"
hide-details
dense
min="1"
:max="pageCount"
@input="options.page = parseInt($event, 10)"
></v-text-field>
</v-col>
<!-- 每頁條數選擇 -->
<v-col cols="12" md="1">
<v-select
v-model="options.itemsPerPage"
:items="[5, 10, 20, 50]"
label="每頁條數"
dense
hide-details
style="width: 100px;"
></v-select>
</v-col>
</v-row>
</v-container>
</div>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" Runat="Server">
<script>
Vue.filter('timeString', function (value, myFormat) {
return value == null || value == "" ? "" : moment(value).format(myFormat || 'YYYY-MM-DD, HH:mm:ss');
});
new Vue({
el: "#app",
vuetify: new Vuetify(vuetify_options),
data() {
return {
registrantList: [],
headers: [
{ text: '登記編號', value: 'registrantCode' },
{ text: '姓名', value: 'name' },
{ text: '電話', value: 'phone' },
{ text: '住址', value: 'address' },
{ text: '登記日期', value: 'registerdate' },
{ text: '費用', value: 'price' },
{ text: '牌位編號', value: 'positionId' },
{ text: '開始時間', value: 'startdate' },
{ text: '結束時間', value: 'enddate' },
{ text: '是否長期', value: 'islongterm' },
{ text: '是否有效', value: 'isactive' },
{ text: '創建時間', value: 'createdat' },
{ text: '', value: 'actions' },
],
options: {
page: 1, // 當前頁
itemsPerPage: 10, // 每頁條數
sortBy: [],
sortDesc: []
},
search: {
registrantUserName: null,
},
total: 0,
loading: false,
}
},
methods: {
getRegistrantListMethod() {
axios.get(HTTP_HOST + 'api/ancestraltablet/registrant/getlist')
.then((res) => {
this.registrantList = res.data;
}).catch((error) => {
alert("載入失敗")
})
},
getRegistrantListByPageMethod() {
if (this.loading) return;
this.loading = true;
axios.post(HTTP_HOST + 'api/ancestraltablet/registrant/getlistbypage', {
page: this.options.page,
pageSize: this.options.itemsPerPage,
searchName: this.search.registrantUserName
})
.then((res) => {
this.registrantList = res.data.data;
this.total = res.data.total;
}).catch((err) => {
console.log(err);
}).finally(() => {
this.loading = false;
});
},
resetTableOptions() {
this.options = {
page: 1,
itemsPerPage: 10,
sortBy: [],
sortDesc: []
};
},
clearSearch() {
this.search.registrantUserName = null;
this.resetTableOptions();
},
handleSearch() {
this.resetTableOptions();
}
},
watch: {
options: {
handler() {
this.getRegistrantListByPageMethod();
},
deep: true,
}
},
mounted() {
this.getRegistrantListByPageMethod()
},
computed: {
pageCount() {
return Math.ceil(this.total / this.options.itemsPerPage)
},
}
})
</script>
</asp:Content>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class admin_ancestraltablet_ancestraltabletuselist_index : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{
}
}

View File

@@ -103,8 +103,8 @@ public partial class admin_follower_reg : MyWeb.config
if (prod.birthday.HasValue)
{
Literal1.Text = publicFun.chagenDate(prod.birthday.Value);
Literal2.Text = Model.follower.chagenSign(prod.birthday.Value);
Literal1.Text = publicFun.chagenDate(prod.birthday.Value); //農曆生日
Literal2.Text = Model.follower.chagenSign(prod.birthday.Value); //生肖
}
if (prod.leader.HasValue)
{

View File

@@ -15,6 +15,15 @@
<div class="col-sm-4 text-left">
<input class="form-control" v-model="guadanorder.order_form.orderNo" readonly />
</div>
<label class="col-sm-2 col-form-label text-center">關聯活動</label>
<div class="col-sm-4">
<select class="form-control" v-model="guadanorder.order_form.activityNum" >
<option :value="null">未關聯</option>
<option v-for="activity in activityList" :key="activity.num" :value="activity.num">
{{activity.subject}}
</option>
</select>
</div>
</div>
<div class="form-group row mt-3">
<label class="col-sm-2 col-form-label text-center">
@@ -85,17 +94,105 @@
<template v-slot:item.name="{item}">
{{item.follower?.u_name}}
</template>
<template #item.actions="{ item }">
<v-btn color="red" variant="outlined" size="small" class="me-2" @click="confirmDeleteGuadanOrderGuest(item)">
取消
</v-btn>
<v-btn color="primary" variant="outlined" size="small" @click="editGuadanOrderGuest(item)">
<v-icon start>mdi-pencil</v-icon>
編輯
</v-btn>
<template v-slot:item.sex="{item}">
{{item.follower?.sex}}
</template>
<template #item.actions="{ item }">
<div>
<!-- 取消預訂 -->
<v-btn
color="red"
variant="outlined"
size="small"
class="me-2"
:disabled="item.statuscode !== '401'"
@click="confirmDeleteGuadanOrderGuest(item)"
>
取消預訂
</v-btn>
<v-btn
color="red"
variant="outlined"
size="small"
class="me-2"
:disabled="item.statuscode !== '401'"
@click="checkinGuadanGuest(item)"
>
入住
</v-btn>
<v-btn
color="red"
variant="outlined"
size="small"
class="me-2"
:disabled="item.statuscode !== '402'"
@click="showXuzhuGuestModalMethod(item)">
續住
</v-btn>
<!-- 退房 -->
<v-btn
color="primary"
variant="outlined"
size="small"
:disabled="item.statuscode !== '402'"
@click="checkoutGuadanOrderGuest(item)"
>
<v-icon start>mdi-exit-run</v-icon>
退房
</v-btn>
</div>
</template>
</v-data-table>
</fieldset>
<!-- 🟢 續住彈出視窗 -->
<div>
<v-dialog v-model="guadanguest.xuzhu.showXuzhuGuestModal" max-width="50%">
<v-card
class="pa-6 d-flex flex-column" style="min-height: 60vh; border-radius: 12px;"
style="min-height: 40vh; border-radius: 12px; box-shadow: 0 8px 20px rgba(0,0,0,0.15);"
>
<!-- 弹窗标题 -->
<v-card-title
class="text-h6 d-flex align-center justify-space-between pb-4"
style="border-bottom: 1px solid #eee;"
>
<div class="d-flex align-center">
<span class="font-weight-bold">续住</span>
</div>
<v-btn icon @click="closeXuzhuGuestModalMethod">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<!-- 弹窗内容 -->
<v-card-text class="flex-grow-1 py-6" style="overflow-y: auto;">
<div class="mb-4">
<span class="font-weight-medium">当前退房时间:</span>
<span class="text-primary">{{ guadanguest.xuzhu.currentCheckoutDate }}</span>
</div>
<div class="d-flex align-center">
<span class="font-weight-medium mr-2">续住后退房时间:</span>
<input
type="date"
id="newCheckoutDate"
v-model="guadanguest.xuzhu.newCheckoutDate"
class="pa-2"
style="border: 1px solid #ccc; border-radius: 6px; padding: 6px 10px;"
/>
</div>
</v-card-text>
<!-- 弹窗操作按钮 -->
<v-card-actions class="justify-end pt-4" style="border-top: 1px solid #eee;">
<v-btn color="primary" class="px-6" @click="xuzhuPost">续住</v-btn>
<v-btn text class="ml-2" @click="closeXuzhuGuestModalMethod">取消</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
<!-- 🟢 掛單蓮友彈出視窗 -->
<div>
@@ -174,21 +271,6 @@
<div class="text-info text-xs mt-1">請從床位清單中選擇</div>
</div>
</v-col>
<v-col cols="12" sm="6">
<div class="d-flex flex-column justify-center" style="height: 100%;">
<v-select
v-model="checkInGuest.inGuest.statusUuid"
:items="checkInGuest.status"
item-value="uuid"
item-text="name"
label="选择個人挂单状态"
dense
outlined
></v-select>
</div>
</v-col>
</v-row>
</v-container>
</v-card-text>
@@ -247,13 +329,13 @@
<div v-for="bed in region_modal.currentSelectBeds" :key="bed.uuid" @click="selectBed(bed)" style="padding: 8px; border: 1px solid #d9d9d9; cursor: pointer; max-height: 250px;" :style="{
backgroundColor: region_modal.currentSelectBed?.uuid === bed.uuid
? '#bae7ff' // 當前選中
: (bed.canUsed ? '#f6ffed' : '#fff1f0'), // 可用綠色,不可用紅色
color: bed.canUsed ? 'black' : '#999', // 不可用時灰色文字
pointerEvents: bed.canUsed ? 'auto' : 'none' // 不可用時無法點擊
: ((bed.canUsed && bed.isActive && !bed.bedIsStop) ? '#f6ffed' : '#fff1f0'), // 可用綠色,不可用紅色
color: (bed.canUsed && bed.isActive && !bed.bedIsStop) ? 'black' : '#999', // 不可用時灰色文字
pointerEvents: (bed.canUsed && bed.isActive && !bed.bedIsStop) ? 'auto' : 'none' // 不可用時無法點擊
}">
<div style="font-weight: 500;">{{ bed.name }}</div>
<div style="margin-top: 4px; font-size: 12px;">
{{ bed.canUsed ? '可用' : '不可用' }}
{{ (!bed.isActive || bed.bedIsStop) ? '停用' : (bed.canUsed ? '可用' : '不可用') }}
</div>
<div
@@ -768,6 +850,7 @@
vuetify: new Vuetify(vuetify_options),
data() {
return {
activityList: [],
availableBedCount: {
male: 0,
female: 0,
@@ -782,6 +865,7 @@
bookerName: null,
bookerPhone: null,
bookerFollowerNum: null,
activityNum: null,
},
status_items: [],
},
@@ -804,6 +888,10 @@
text: '姓名',
value: 'name'
},
{
text: '性別',
value: 'sex'
},
{
text: '掛單開始時間',
value: 'checkinat'
@@ -812,10 +900,6 @@
text: '掛單結束時間',
value: 'checkoutat'
},
{
text: '房間',
value: 'roomName'
},
{
text: '床位',
value: 'bedName'
@@ -835,6 +919,13 @@
],
items: [],
showCreateGuestModal: false,
xuzhu: {
showXuzhuGuestModal: false,
currentCheckoutDate: null,
newCheckoutDate: null,
guestUuid: null,
guestBedUuid: null,
}
},
checkInGuest: {
showSelectGuadanOrderGuest: false,
@@ -848,7 +939,7 @@
bedUuid: null,
checkInAt: null,
checkOutAt: null,
statusUuid: null,
statuscode: null,
},
status: [],
},
@@ -938,8 +1029,97 @@
}
},
methods: {
//续住相關方法--------------------start
showXuzhuGuestModalMethod(guest) {
if (!guest.checkoutat) {
return;
}
this.guadanguest.xuzhu.showXuzhuGuestModal = true;
this.guadanguest.xuzhu.currentCheckoutDate = guest.checkoutat;
this.guadanguest.xuzhu.guestUuid = guest.uuid;
this.guadanguest.xuzhu.guestBedUuid = guest.bedUuid;
this.$nextTick(() => { // 确保弹窗 DOM 已渲染
const input = document.getElementById('newCheckoutDate');
if (input) {
const checkoutDate = new Date(guest.checkoutat); // 用指定日期
checkoutDate.setDate(checkoutDate.getDate() + 1); // 明天
const year = checkoutDate.getFullYear();
const month = String(checkoutDate.getMonth() + 1).padStart(2, '0');
const day = String(checkoutDate.getDate()).padStart(2, '0');
const tomorrow = `${year}-${month}-${day}`;
input.min = tomorrow; // 限制最小值
//input.value = tomorrow; // 默认选中明天
}
});
console.log(guest.checkoutat)
},
closeXuzhuGuestModalMethod() {
console.log(this.guadanguest.xuzhu.newCheckoutDate)
this.guadanguest.xuzhu.showXuzhuGuestModal = false;
this.guadanguest.xuzhu.currentCheckoutDate = null;
this.guadanguest.xuzhu.newCheckoutDate = null;
this.guadanguest.xuzhu.guestUuid = null;
this.guadanguest.xuzhu.guestBedUuid = null;
console.log(this.guadanguest.xuzhu.newCheckoutDate)
console.log(this.guadanguest.xuzhu.currentCheckoutDate)
console.log(this.guadanguest.xuzhu.guestUuid)
console.log(this.guadanguest.xuzhu.guestBedUuid)
},
xuzhuPost() {
// 校验必填
if (!this.guadanguest.xuzhu.guestUuid || !this.guadanguest.xuzhu.guestBedUuid) {
alert("GuestUuid 和 GuestBedUuid 不能为空");
return;
}
if (!this.guadanguest.xuzhu.newCheckoutDate || !this.guadanguest.xuzhu.currentCheckoutDate) {
alert("续住时间不能为空");
return;
}
const payload = {
guestUuid: this.guadanguest.xuzhu.guestUuid,
guestBedUuid: this.guadanguest.xuzhu.guestBedUuid,
currentCheckoutDate: this.guadanguest.xuzhu.currentCheckoutDate,
newCheckoutDate: this.guadanguest.xuzhu.newCheckoutDate
};
axios.post(HTTP_HOST + 'api/guadanorderguest/xuzhu', payload)
.then((res) => {
this.$refs.messageModal.open({
title: '续住成功',
message: '客人续住已处理',
status: 'success',
callback: () => {
// 弹窗关闭后的回调
try {
this.getGuadanOrderGuestByOrderNo();
}
catch (error) {
console.error("发生错误:", error.message);
} finally {
this.closeXuzhuGuestModalMethod();
}
}
});
})
.catch((error) => {
this.$refs.messageModal.open({
title: '续住失败',
message: error.response?.data?.message || '系统异常,请稍后重试',
status: 'error'
});
});
},
//续住相關方法--------------------end
getActivityList() {
axios.post(HTTP_HOST + 'api/activity/GetList?page=1&pageSize=500', { kind: 0, subject: "" })
.then((res) => {
this.activityList = res.data.list
})
},
getavailablebedcountbytime(startTime, endTime) {
axios.get('/api/region/bed/getavailablebedcountbytime',{
axios.get(HTTP_HOST + 'api/region/bed/getavailablebedcountbytime', {
params: {
startTime: startTime,
endTime: endTime
@@ -961,7 +1141,7 @@
},
confirmAllocation() {
//確認分配
axios.post('/api/region/bed/confirmallocation', {
axios.post(HTTP_HOST + 'api/region/bed/confirmallocation', {
preBeds: this.automaticBedAllocation.preBeds,
orderNo: this.guadanorder.order_form.orderNo,
checkInAt: this.guadanorder.order_form.startdate,
@@ -970,6 +1150,10 @@
.then(res => {
this.resetAutomaticBedAllocation();
this.getGuadanOrderGuestByOrderNo();
}).catch((error) => {
this.$refs.messageModal.open({
message: (error.response?.data?.message || error.message)
});
});
},
@@ -985,7 +1169,7 @@
CheckInAt: this.guadanorder.order_form.startdate || new Date(), // 入住時間
CheckOutAt: this.guadanorder.order_form.enddate || null // 退房時間,可為空
};
axios.post('/api/region/bed/preallocation', payload)
axios.post(HTTP_HOST + 'api/region/bed/preallocation', payload)
.then(res => {
this.automaticBedAllocation.preBeds = res.data.data;
})
@@ -1033,7 +1217,7 @@
getMultiSelectFollowers: function () {
var fm = this.automaticBedAllocation.followerModal;
var self = this;
axios.post('/api/lianyou/getfollowers', null, {
axios.post(HTTP_HOST + 'api/lianyou/getfollowers', null, {
params: {
page: fm.page,
pageSize: fm.pageSize,
@@ -1104,7 +1288,7 @@
},
getGuadanOrderById() {
if (this.guadanorder.order_form.uuid) {
axios.get('/api/guadan/getorderbyid', {
axios.get(HTTP_HOST + 'api/guadan/getorderbyid', {
params: {
orderId: this.guadanorder.order_form.uuid
}
@@ -1117,12 +1301,13 @@
this.guadanorder.order_form.bookerPhone = res.data.bookerPhone;
this.guadanorder.order_form.bookerFollowerNum = res.data.bookerFollowerNum;
this.guadanorder.order_form.uuid = res.data.uuid;
this.guadanorder.order_form.activityNum = res.data.activityNum;
})
}
},
getGuadanOrderGuestByOrderNo() {
if (this.guadanorder.order_form.orderNo) {
axios.get('/api/guadanorderguest/getbyorderno', {
axios.get(HTTP_HOST + 'api/guadanorderguest/getbyorderno', {
params: {
orderNo: this.guadanorder.order_form.orderNo
}
@@ -1132,7 +1317,7 @@
}
},
getGuadanOrderStatus() {
axios.get('/api/region/guadan/status/list')
axios.get(HTTP_HOST + 'api/region/guadan/status/list')
.then((res) => {
this.guadanorder.status_items = res.data;
})
@@ -1141,7 +1326,7 @@
if (!this.validateOrderForm()) {
return;
}
axios.post('/api/guadan/create', this.guadanorder.order_form)
axios.post(HTTP_HOST + 'api/guadan/create', this.guadanorder.order_form)
.then((res => {
this.$refs.messageModal.open({
title: '掛單提示',
@@ -1165,7 +1350,7 @@
if (!this.validateOrderForm()) {
return;
}
axios.post('/api/guadan/update', this.guadanorder.order_form)
axios.post(HTTP_HOST + 'api/guadan/update', this.guadanorder.order_form)
.then((res => {
this.$refs.messageModal.open({
title: '掛單提示',
@@ -1203,6 +1388,40 @@
//掛單相關方法-------------------end
//增加蓮友方法-------------------start
checkoutGuadanOrderGuest(guest) {
this.$refs.confirmModal.open({
message: `確定要將 ${guest.follower.u_name || ''} 退房嗎?`,
onConfirm: async () => {
try {
const response = await axios.post(HTTP_HOST + HTTP_HOST + `api/guadanorderguest/checkout`, null, {
params: { uuid: guest.uuid }
});
// 成功提示
this.$refs.messageModal.open({
title: '操作成功',
message: '退房成功!',
status: 'success'
});
// 更新狀態並刷新資料
guest.statusCode = "403"; // 已退房
this.getGuadanOrderGuestByOrderNo();
} catch (error) {
console.error(error);
// 失敗提示
this.$refs.messageModal.open({
title: '操作失敗',
message: error.response?.data?.message || '退房過程中發生錯誤!',
status: 'error'
});
}
}
});
},
resetInGuest() {
this.checkInGuest.inGuest = {
uuid: null,
@@ -1212,7 +1431,7 @@
bedUuid: null,
checkInAt: null,
checkOutAt: null,
statusUuid: null,
statuscode: null,
};
},
setInGuest() {
@@ -1276,16 +1495,15 @@
},
createCheckInGuest() {
return axios.post('/api/guadanorderguest/create', this.checkInGuest.inGuest)
return axios.post(HTTP_HOST + 'api/guadanorderguest/create', this.checkInGuest.inGuest)
},
checkBedAndFollower() {
this.checkInGuest.inGuest
},
getGuadanGuestStatus() {
axios.get('/api/region/bed/status/list')
axios.get(HTTP_HOST + 'api/region/bed/status/list')
.then((res) => {
this.checkInGuest.status = res.data.filter(item => item.category === 4);
console.log(this.checkInGuest.status);
this.checkInGuest.status = res.data.filter(item => item.category === 4 && item.code != '404');
})
},
//增加蓮友方法-------------------end
@@ -1306,7 +1524,7 @@
pageSize: itemsPerPage,
searchName: this.selectGuestModal.searchNameOrPhone
};
axios.post('/api/lianyou/getfollowers', null, {
axios.post(HTTP_HOST + 'api/lianyou/getfollowers', null, {
params: params
}).then((res) => {
this.selectGuestModal.items = res.data.data
@@ -1318,7 +1536,7 @@
},
selectGuadanOrderGuest(guest) {
this.selectGuestModal.currentSelectedGuest = guest;
console.log('----------'+ guest)
console.log('----------' + guest)
this.selectGuestModal.fullNameText = guest.u_name;
this.checkInGuest.inGuest.followerNum = guest.num;
this.selectGuestModal.showSelectGuestModal = false;
@@ -1336,12 +1554,12 @@
this.getCurrentSelectBedTextByBedId(guest.bedUuid);
this.selectGuestModal.fullNameText = guest.follower?.u_name;
this.selectGuestModal.currentSelectedGuest = guest.follower;
this.checkInGuest.inGuest.statusUuid = guest.statusUuid;
this.checkInGuest.inGuest.statuscode = guest.statuscode;
},
async saveEditGuadanOrderGuest() {
try {
const res = await axios.post('/api/guadanorderguest/update', this.checkInGuest.inGuest)
const res = await axios.post(HTTP_HOST + 'api/guadanorderguest/update', this.checkInGuest.inGuest)
this.getGuadanOrderGuestByOrderNo();
this.closeCheckInModal();
} catch (error) {
@@ -1352,9 +1570,14 @@
},
deleteGuadanOrderGuest(guest) {
axios.post('/api/guadanorderguest/delete?uuid=' + guest.uuid).then((res) => {
this.guadanguest.items = this.guadanguest.items.filter(i => i.uuid != guest.uuid);
})
axios.post(HTTP_HOST + 'api/guadanorderguest/cancel?uuid=' + guest.uuid)
.then((res) => {
this.guadanguest.items = this.guadanguest.items.filter(i => i.uuid != guest.uuid);
}).catch((error) => {
this.$refs.messageModal.open({
message: (error.response?.data?.message || error.message)
})
});
},
confirmDeleteGuadanOrderGuest(guest) {
this.$refs.confirmModal.open({
@@ -1364,11 +1587,48 @@
}
})
},
async checkinGuadanGuest(guest) {
// 先確認操作
this.$refs.confirmModal.open({
message: '確認入住?',
onConfirm: async () => {
try {
// 發送請求到後端 API
const response = await axios.post(HTTP_HOST + `api/guadanorderguest/checkin`, null, {
params: { uuid: guest.uuid }
});
// 成功提示
this.$refs.messageModal.open({
title: '操作成功',
message: '入住成功!',
status: 'success'
});
// 更新本地列表,修改狀態為已入住 (402)
guest.statusCode = "402";
// 如果需要刷新整個列表,也可以調用
this.getGuadanOrderGuestByOrderNo();
} catch (error) {
console.error(error);
// 失敗提示
this.$refs.messageModal.open({
title: '操作失敗',
message: error.response?.data?.message || '入住過程中發生錯誤!',
status: 'error'
});
}
}
});
},
//蓮友選擇相關方法---------------end
//床位選擇相關方法----------------start
async loadRegions() {
const res = await axios.post('/api/region/getRegionList');
const res = await axios.post(HTTP_HOST + 'api/region/getRegionList');
this.region_modal.regions = res.data;
},
async loadRegionsByGender() {
@@ -1383,7 +1643,7 @@
}
}
const res = await axios.post('/api/region/getRegionListByGender', {
const res = await axios.post(HTTP_HOST + 'api/region/getRegionListByGender', {
IsMale: isMale
});
@@ -1400,7 +1660,7 @@
this.region_modal.selectedType = 'room';
this.region_modal.currentSelectBeds = room.beds;
if (this.checkInGuest.inGuest.checkInAt && this.checkInGuest.inGuest.checkOutAt) {
axios.get('/api/region/room/bed/list', {
axios.get(HTTP_HOST + 'api/region/room/bed/list', {
params: {
roomUuid: room.uuid,
StartTime: this.checkInGuest.inGuest.checkInAt,
@@ -1418,7 +1678,7 @@
},
GetRegionRoomBedListByRoomId(roomUuid) {
if (this.checkInGuest.inGuest.checkInAt && this.checkInGuest.inGuest.checkOutAt) {
axios.get('/api/region/bed/list')
axios.get(HTTP_HOST + 'api/region/bed/list')
.then((res) => {
})
@@ -1532,6 +1792,7 @@
this.loadRegions();
this.getGuadanOrderStatus();
this.getGuadanGuestStatus();
this.getActivityList();
},
computed: {

View File

@@ -0,0 +1,239 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="index.aspx.cs" Inherits="admin_guadan_guest_index" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" Runat="Server">
<nav class="search-bar">
<div class="form-item">
<label>姓名:</label>
<input v-model="search.searchName" type="text" placeholder="請輸入姓名">
</div>
<div class="form-item">
<label>入住日期:</label>
<input v-model="search.searchCheckInDate" type="date">
</div>
<div class="form-item">
<label>退房日期:</label>
<input type="date" v-model="search.searchCheckOutDate">
</div>
<div class="form-item">
<label>入住日期區間:</label>
<input type="date" v-model="search.searchCheckInDateStart"> -
<input type="date" v-model="search.searchCheckInDateEnd">
</div>
<div class="form-item">
<label>退房日期區間:</label>
<input type="date" v-model="search.searchCheckOutDateStart"> -
<input type="date" v-model="search.searchCheckOutDateEnd">
</div>
<div class="form-item buttons">
<button @click="handleSearch" type="button">查詢</button>
<button type="button" @click="clearSearch">清除查詢條件</button>
</div>
</nav>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<div>
<v-data-table
:headers="headers"
:items="guests"
:options.sync="options"
:server-items-length="total"
:loading="loading"
hide-default-footer
class="elevation-1"
>
<template v-slot:top>
<v-toolbar flat>
<v-toolbar-title>入住人列表</v-toolbar-title>
<v-spacer></v-spacer>
</v-toolbar>
</template>
<template #item.checkindate ="{item}">
{{item.checkindate|timeString('YYYY-MM-DD')}}
</template>
<template #item.checkoutdate ="{item}">
{{item.checkoutdate|timeString('YYYY-MM-DD')}}
</template>
<template #item.guadanorderno="{item}">
{{item.guadanorderno}}<a :href="'/admin/guadan/create.aspx?orderId='+item.guadanorderno" class="btn btn-outline-primary">查看掛單</a>
</template>
</v-data-table>
<v-container>
<v-row class="align-baseline" wrap>
<v-col cols="12" md="9">
<v-pagination
v-model="options.page"
:length="pageCount">
</v-pagination>
</v-col>
<v-col class="text-truncate text-right" cols="12" md="2">
共 {{ total }} 筆, 頁數:
</v-col>
<v-col cols="6" md="1">
<v-text-field
v-model="options.page"
type="number"
hide-details
dense
min="1"
:max="pageCount"
@input="options.page = parseInt($event, 10)"
></v-text-field>
</v-col>
</v-row>
</v-container>
</div>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" Runat="Server">
<style>
.search-bar {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
background: #f9f9f9;
border-radius: 8px;
}
.form-item {
display: flex;
align-items: center;
gap: 6px;
}
input {
padding: 4px 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
padding: 6px 14px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #66b1ff;
}
</style>
<script>
Vue.filter('timeString', function (value, myFormat) {
return value == null || value == "" ? "" : moment(value).format(myFormat || 'YYYY-MM-DD, HH:mm:ss');
});
new Vue({
el: '#app',
vuetify: new Vuetify(vuetify_options),
data() {
return {
loading: false,
search: {
searchName: null,
searchCheckInDateStart: null,//入住日期開始
searchCheckInDateEnd: null,//入住日期的結束
searchCheckOutDateStart: null,//退房日期的開始
searchCheckOutDateEnd: null,//退房日期的結束
searchCheckInDate: null,
searchCheckOutDate: null,
},
options: {
page: 1, // 當前頁
itemsPerPage: 10, // 每頁條數
sortBy: [],
sortDesc: []
},
headers: [
{ text: '姓名', value: 'name' },
{ text: '掛單單號', value: 'guadanorderno' },
{ text: '入住日期', value: 'checkindate' },
{ text: '退房日期', value: 'checkoutdate' },
{ text: '房間號', value: 'roomName' },
{ text: '狀態', value: 'statusName'},
],
guests: [], // 表格數據
total: 0,
}
},
methods: {
resetTableOptions() {
this.options = {
page: 1,
itemsPerPage: 10,
sortBy: [],
sortDesc: []
};
},
handleSearch() {
this.resetTableOptions();
},
clearSearch() {
this.search.searchName = null;
this.search.searchCheckInDate = null;
this.search.searchCheckOutDate = null;
this.search.searchCheckInDateStart = null;
this.search.searchCheckInDateEnd = null;
this.search.searchCheckOutDateStart = null;
this.search.searchCheckOutDateEnd = null;
this.resetTableOptions();
},
fetchGuests() {
if (this.search.searchName && this.search.searchName.includes(' ')) {
alert('搜索內容不能包含空格');
return; // 阻止繼續執行
}
if (this.loading) return;
this.loading = true;
axios.post(HTTP_HOST + 'api/guadan/guest/query/list',
{
page: this.options.page,
pageSize: this.options.itemsPerPage,
searchName: this.search.searchName,
searchCheckInDate: this.search.searchCheckInDate,
searchCheckOutDate: this.search.searchCheckOutDate,
searchCheckInDateStart: this.search.searchCheckInDateStart,
searchCheckInDateEnd: this.search.searchCheckInDateEnd,
searchCheckOutDateStart: this.search.searchCheckOutDateStart,
searchCheckOutDateEnd: this.search.searchCheckOutDateEnd,
}).then(res => {
this.guests = res.data.items; // 數據
this.total = res.data.total; // 總數
}).finally(() => {
this.loading = false;
});
}
},
watch: {
options: {
handler() {
this.fetchGuests(); // 監聽分頁、排序變化,自動載入數據
},
deep: true,
}
},
mounted() {
},
computed: {
pageCount() {
return Math.ceil(this.total / this.options.itemsPerPage)
},
}
})
</script>
</asp:Content>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class admin_guadan_guest_index : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{
}
}

View File

@@ -6,16 +6,45 @@
<nav>
<a href="create.aspx" class="btn btn-primary" >新建掛單</a>
</nav>
<div class="d-flex align-items-center gap-3">
<label class="mb-0">掛單單號</label>
<input class="form-control w-auto" style="width:150px;" v-model="search.guaDanOrderNo" />
<label class="mb-0">掛單登記人</label>
<input class="form-control w-auto" style="width:150px;" v-model="search.guadanUser" />
<label class="mb-0">開始時間</label>
<input class="form-control w-auto" style="width:150px;" type="date" v-model="search.startDate" />
<label class="mb-0">結束時間</label>
<input class="form-control w-auto" style="width:150px;" type="date" v-model="search.endDate" />
<button class="btn btn-primary" type="button" @click="handleSearch">查詢</button>
<button class="btn btn-outline-primary" type="button" @click="clearSearch">清除條件</button>
</div>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<div class="mx-5">
<v-data-table
:items="items"
:headers="headers">
:headers="headers"
:item-class="item => item.is_timeout ? 'row-timeout' : ''"
hide-default-footer>
<template #item.actions="{item}">
<a :href="'create.aspx?orderId='+item.guaDanOrderNo" class="btn btn-secondary">編輯</a>
<a class="btn btn-outline-danger" @click="deleteGuadanOrder(item)">取消</a>
<a :href="'view.aspx?orderId='+item.guaDanOrderNo" class="btn btn-primary">查看</a>
<a :href="'create.aspx?orderId='+item.guaDanOrderNo" class="btn btn-secondary"
:style="item.guadan_status?.code == 502 ? 'pointer-events: none; opacity: 0.5; cursor: not-allowed;' : ''">編輯</a>
<a
class="btn btn-outline-danger"
href="#"
:style="item.guest_count != 0 ? 'pointer-events: none; opacity: 0.5; cursor: not-allowed;' : ''"
@click.prevent="item.guest_count != 0 ? null : deleteGuadanOrder(item)"
>
取消
</a>
</template>
<template #item.room="{item}">
{{item.room.name}}
@@ -23,19 +52,60 @@
<template #item.bed="{item}">
{{item.bed.name}}
</template>
<template #item.status="{item}">
{{item.status}}
<template #item.guadan_status="{item}">
{{item.guadan_status?.name}}
</template>
<template #item.start_date="{item}">
{{item.start_date | timeString('YYYY/MM/DD HH:mm')}}
{{item.start_date | timeString('YYYY/MM/DD')}}
</template>
<template #item.end_date="{item}">
{{item.end_date | timeString('YYYY/MM/DD HH:mm')}}
{{item.end_date | timeString('YYYY/MM/DD')}}
</template>
<template #item.created_at="{item}">
{{item.created_at | timeString('YYYY/MM/DD HH:mm')}}
</template>
<template #item.activity="{item}">
{{item.activity?.subject}}
</template>
<template #item.is_timeout="{item}">
{{item.is_timeout ? '已超時': '否'}}
</template>
</v-data-table>
<v-container>
<v-row class="align-baseline" wrap="false">
<v-col cols="12" md="8">
<v-pagination
v-model="options.page"
:length="pageCount">
</v-pagination>
</v-col>
<v-col class="text-truncate text-right" cols="12" md="2">
共 {{ total }} 筆, 頁數:
</v-col>
<v-col cols="6" md="1">
<v-text-field
v-model="options.page"
type="number"
hide-details
dense
min="1"
:max="pageCount"
@input="options.page = parseInt($event, 10)"
></v-text-field>
</v-col>
<!-- 每頁條數選擇 -->
<v-col cols="12" md="1">
<v-select
v-model="options.itemsPerPage"
:items="[5, 10, 20, 50]"
label="每頁條數"
dense
hide-details
style="width: 100px;"
></v-select>
</v-col>
</v-row>
</v-container>
</div>
<!-- 更新修改確認彈出視窗 -->
<message-modal ref="messageModal"></message-modal>
@@ -56,32 +126,100 @@
return {
items: [],
headers: [
{ text: '登记挂单莲友', value: 'bookerName'},
{ text: '登記掛單蓮友', value: 'bookerName' },
{ text: '掛單單號', value: 'guaDanOrderNo'},
{ text: '起始日期', value: 'start_date', align: 'center' },
{ text: '結束日期', value: 'end_date', align: 'center' },
{ text: '掛單人數', value: 'guest_count' },
{ text: '狀態', value: 'statusName', align: 'center' },
{ text: '狀態', value: 'guadan_status', align: 'center' },
{ text: '建立時間', value: 'created_at', align: 'center' },
{ text: '備註', value: 'notes', align: 'center' },
{ text: '關聯活動', value: 'activity', align: 'center' },
{ text: '超時退房', value: 'is_timeout', align: 'center' },
{ text: '操作', value: 'actions', align: 'center' }
],
options: {
page: 1, // 當前頁
itemsPerPage: 10, // 每頁條數
sortBy: [],
sortDesc: []
},
search: {
startDate: null,
endDate: null,
guadanUser: null,
guaDanOrderNo: null,
},
total: 0,
loading: false,
}
},
methods: {
resetTableOptions() {
this.options = {
page: 1,
itemsPerPage: 10,
sortBy: [],
sortDesc: []
};
},
handleSearch() {
let orderNo = this.search.guaDanOrderNo;
if (orderNo) {
orderNo = orderNo.replace(/\s+/g, '');
this.search.guaDanOrderNo = orderNo;
}
const val = this.search.guadanUser;
// 驗證是否包含空格
if (val && /\s/.test(val)) {
this.$refs.messageModal.open({
message: '掛單登記人不能包含空格'
});
return;
}
// 驗證長度
if (val && val.length > 10) {
this.$refs.messageModal.open({
message: '掛單登記人不能超過 10 個字'
});
return;
}
this.resetTableOptions();
},
clearSearch() {
this.search.startDate = null;
this.search.endDate = null;
this.search.guadanUser = null;
this.search.guaDanOrderNo = null;
this.resetTableOptions();
},
getGuadanOrder() {
axios.get('/api/guadan/list')
if (this.loading) return;
axios.post(HTTP_HOST + 'api/guadan/list', {
startDate: this.search.startDate,
endDate: this.search.endDate,
guadanUser: this.search.guadanUser,
guaDanOrderNo: this.search.guaDanOrderNo,
page: this.options.page,
pageSize: this.options.itemsPerPage
})
.then((res) => {
this.items = res.data;
this.items = res.data.data;
this.total = res.data.total;
}).catch((err) => {
console.log(err);
})
}).finally(() => {
this.loading = false;
});
},
deleteGuadanOrder(order) {
this.$refs.confirmModal.open({
message: '確認取消掛單?',
onConfirm: () => {
axios.post('/api/guadan/delete', null, {
axios.post(HTTP_HOST + 'api/guadan/cancel', null, {
params: {
uuid: order.uuid
}
@@ -92,7 +230,7 @@
})
}).catch((error) => {
this.$refs.messageModal.open({
message: '取消失敗'
message: error.response?.data || '取消失敗'
})
})
}
@@ -101,12 +239,27 @@
},
},
watch: {
options: {
handler() {
this.getGuadanOrder(); // 監聽分頁、排序變化,自動載入數據
},
deep: true,
}
},
mounted() {
this.getGuadanOrder();
},
computed: {
pageCount() {
return Math.ceil(this.total / this.options.itemsPerPage)
},
}
});
</script>
</asp:Content>
<style>
.row-timeout {
background-color: #ffdddd !important;
}
</style>
</asp:Content>

View File

@@ -1,143 +0,0 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="statistics.aspx.cs" Inherits="admin_guadan_statistics" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" Runat="Server">
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<div class="container my-4">
<!-- 客房统计 -->
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 g-3">
<div class="col">
<div class="p-3 bg-light text-center rounded shadow" style="min-height: 180px;">
<div class="fs-2">🏠</div>
<div class="text-muted small mt-1">总房间数量</div>
<div class="fw-bold fs-5 mt-1">{{ roomStatistics.roomCount }}</div>
</div>
</div>
<div class="col">
<div class="p-3 bg-light text-center rounded shadow" style="min-height: 180px;">
<div class="fs-2">🚪</div>
<div class="text-muted small mt-1">空房间数量</div>
<div class="fw-bold fs-5 mt-1">{{ roomStatistics.emptyRoomCount }}</div>
</div>
</div>
<div class="col">
<div class="p-3 bg-light text-center rounded shadow" style="min-height: 180px;">
<div class="fs-2">🛏️</div>
<div class="text-muted small mt-1">总床位数量</div>
<div class="fw-bold fs-5 mt-1">
{{ roomStatistics.bedCount }} (男:{{ roomStatistics.maleBedCount }},女:{{ roomStatistics.femaleBedCount }}
</div>
</div>
</div>
<div class="col">
<div class="p-3 bg-light text-center rounded shadow" style="min-height: 180px;">
<div class="fs-2">🛌</div>
<div class="text-muted small mt-1">可用空床</div>
<div class="fw-bold fs-5 mt-1">
{{ roomStatistics.emptyBedCount }} (男:{{ roomStatistics.emptyMaleBedCount }},女:{{ roomStatistics.emptyFemaleBedCount }}
</div>
</div>
</div>
</div>
<!-- 挂单统计 -->
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 g-3 mt-1">
<div class="col">
<div class="p-3 bg-light text-center rounded shadow" style="min-height: 180px;">
<div class="fs-2">📝</div>
<div class="text-muted small mt-1">总挂单次数</div>
<div class="fw-bold fs-5 mt-1">{{ guadanStatistics.guadanTotalCount }}</div>
</div>
</div>
<div class="col">
<div class="p-3 bg-light text-center rounded shadow" style="min-height: 180px;">
<div class="fs-2">📋</div>
<div class="text-muted small mt-1">当前挂单数量</div>
<div class="fw-bold fs-5 mt-1">{{ guadanStatistics.guadanCurrentCount }}</div>
</div>
</div>
<div class="col">
<div class="p-3 bg-light text-center rounded shadow" style="min-height: 180px;">
<div class="fs-2">👥</div>
<div class="text-muted small mt-1">总挂单人数</div>
<div class="fw-bold fs-5 mt-1">
{{ guadanStatistics.guadanPeopleTotal }} (男:{{ guadanStatistics.guadanPeopleMale }},女:{{ guadanStatistics.guadanPeopleFemale }}
</div>
</div>
</div>
<div class="col">
<div class="p-3 bg-light text-center rounded shadow" style="min-height: 180px;">
<div class="fs-2">👨👩</div>
<div class="text-muted small mt-1">当前挂单人数</div>
<div class="fw-bold fs-5 mt-1">
{{ guadanStatistics.guadanPeopleCurrent }} (男:{{ guadanStatistics.guadanPeopleCurrentMale }},女:{{ guadanStatistics.guadanPeopleCurrentFemale }}
</div>
</div>
</div>
</div>
</div>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" Runat="Server">
<script>
new Vue({
el: '#app',
vuetify: new Vuetify(vuetify_options),
data() {
return {
roomStatistics: {
roomCount: 0,
emptyRoomCount: 0,
bedCount: 0,
maleBedCount: 0,
femaleBedCount: 0,
emptyBedCount: 0,
emptyMaleBedCount: 0,
emptyFemaleBedCount: 0
},
guadanStatistics: {
guadanTotalCount: 0,
guadanCurrentCount: 0,
guadanPeopleTotal: 0,
guadanPeopleMale: 0,
guadanPeopleFemale: 0,
guadanPeopleCurrent: 0,
guadanPeopleCurrentMale: 0,
guadanPeopleCurrentFemale: 0
},
}
},
methods: {
GetGuadanStatistics() {
axios.get('/api/guadanStatistics/GetGuadanStatistics')
.then((res) => {
this.roomStatistics = res.data.roomStatistics;
this.guadanStatistics = res.data.guadanStatistics;
})
}
},
watch: {
},
mounted() {
this.GetGuadanStatistics();
// 每两分钟更新一次 (2 * 60 * 1000 毫秒)
setInterval(() => {
this.GetGuadanStatistics();
}, 1 * 60 * 1000);
},
})
</script>
</asp:Content>

View File

@@ -0,0 +1,348 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="statistics_table.aspx.cs" Inherits="admin_guadan_statistics_table" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" Runat="Server">
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<!-- 今日使用情況 -->
<div class="section mb-6">
<v-card outlined class="pa-1">
<v-card-title class="headline grey--text text--darken-2">
掛單統計
</v-card-title>
<v-divider class="mb-4"></v-divider>
<v-card-text>
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 g-3 mt-1">
<div class="col">
<div class="p-3 bg-light text-center rounded shadow" style="min-height: 180px;">
<div class="fs-2">📝</div>
<div class="text-muted small mt-1">总挂单次数</div>
<div class="fw-bold fs-5 mt-1">{{ guadanStatistics.guadanTotalCount }}</div>
</div>
</div>
<div class="col">
<div class="p-3 bg-light text-center rounded shadow" style="min-height: 180px;">
<div class="fs-2">📋</div>
<div class="text-muted small mt-1">当前挂单数量</div>
<div class="fw-bold fs-5 mt-1">{{ guadanStatistics.guadanCurrentCount }}</div>
</div>
</div>
<div class="col">
<div class="p-3 bg-light text-center rounded shadow" style="min-height: 180px;">
<div class="fs-2">👥</div>
<div class="text-muted small mt-1">总挂单人数</div>
<div class="fw-bold fs-5 mt-1">
{{ guadanStatistics.guadanPeopleTotal }} (男:{{ guadanStatistics.guadanPeopleMale }},女:{{ guadanStatistics.guadanPeopleFemale }}
</div>
</div>
</div>
<div class="col">
<div class="p-3 bg-light text-center rounded shadow" style="min-height: 180px;">
<div class="fs-2">👨👩</div>
<div class="text-muted small mt-1">已預約掛單人數</div>
<div class="fw-bold fs-5 mt-1">
{{ guadanStatistics.guadanPeopleCurrent }} (男:{{ guadanStatistics.guadanPeopleCurrentMale }},女:{{ guadanStatistics.guadanPeopleCurrentFemale }}
</div>
</div>
</div>
</div>
</v-card-text>
</v-card>
</div>
<!-- 近期床位使用統計 -->
<div class="section container">
<!-- 日期筛选区 -->
<div class="d-flex align-center flex-wrap" style="gap: 5px;">
<!-- 开始日期 -->
<v-menu
ref="menu1"
v-model="menu1"
:close-on-content-click="false"
transition="scale-transition"
offset-y
min-width="auto"
>
<template v-slot:activator="{ on, attrs }">
<v-text-field
v-model="startDate"
label="開始日期"
prepend-icon="mdi-calendar"
readonly
v-bind="attrs"
v-on="on"
dense
outlined
></v-text-field>
</template>
<v-date-picker v-model="startDate" @input="menu1 = false"></v-date-picker>
</v-menu>
<!-- 结束日期 -->
<v-menu
ref="menu2"
v-model="menu2"
:close-on-content-click="false"
transition="scale-transition"
offset-y
min-width="auto"
>
<template v-slot:activator="{ on, attrs }">
<v-text-field
v-model="endDate"
label="結束日期"
prepend-icon="mdi-calendar"
readonly
v-bind="attrs"
v-on="on"
dense
outlined
></v-text-field>
</template>
<v-date-picker v-model="endDate" @input="menu2 = false"></v-date-picker>
</v-menu>
<!-- 查询按钮 -->
<v-btn
color="primary"
@click="getList"
style="align-self: stretch;"
>
查詢
</v-btn>
<v-btn
color="primary"
style="align-self: stretch;"
@click="exportStatisticsToExcel">
導出下面表格數據到Excel
</v-btn>
</div>
</div>
<v-divider class="mb-4"></v-divider>
<div>
<!-- 表格 -->
<v-data-table
:items="items"
:headers="headers"
class="elevation-2"
dense
hide-default-footer
:items-per-page="10"
>
<template #item.date="{ item }">
<span>{{ item.date | timeString('YYYY-MM-DD') }}</span>
</template>
<template #item.todaytotalbookers="{item}">
<span>
{{item?.todaytotalbookers + '(男: ' + item.bookmale + ', 女:' + item.bookfemale + ')'}}
</span>
<button type="button" class="btn btn-outline-primary" @click="showBookDialog(item, '已預約','booking')">查看預約人</button>
</template>
<template #item.checkin="{item}">
<span>
{{item?.checkin + '(男: ' + item.checkinmale + ', 女:' + item.checkinfemale + ')'}}
</span>
<button type="button" class="btn btn-outline-primary" @click="showBookDialog(item, '已入住', 'checkin')">查看入住人</button>
</template>
<template #item.bedusagerate="{ item }">
{{ ((item.todaytotalbookers / bedcount) * 100).toFixed(2) + '%' }}
</template>
<template #item.roomcount="{item}">
{{roomcount}}
</template>
<template #item.bedcount="{item}">
{{bedcount}}
</template>
</v-data-table>
</div>
<div>
<v-dialog v-model="bookerDialog.show" max-width="500">
<v-card
style="width: 40vw; height: 50vh; display: flex; flex-direction: column;"
>
<!-- 标题 -->
<v-card-title class="text-h5" style="flex: 0 0 auto; background-color: #1976D2; color: white; padding: 16px;">
{{ bookerDialog.title }}
</v-card-title>
<!-- 内容撑满 -->
<v-card-text
style="flex: 1; overflow-y: auto;"
>
<v-data-table
:items="bookerDialog.items"
:headers="bookerDialog.headers">
</v-data-table>
</v-card-text>
<!-- 底部按钮 -->
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="closeBookDialog">關閉</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" Runat="Server">
<script>
Vue.filter('timeString', function (value, myFormat) {
return value == null || value == "" ? "" : moment(value).format(myFormat || 'YYYY-MM-DD, HH:mm:ss');
});
new Vue({
el: '#app',
vuetify: new Vuetify(vuetify_options),
data() {
return {
items: [],
headers: [
{ text: '日期', value: 'date' },
{ text: '房间数量', value: 'roomcount' },
{ text: '床位数量', value: 'bedcount' },
{ text: '预约人数', value: 'todaytotalbookers' },
{ text: '已入住人数', value: 'checkin'},
{ text: '可用床位', value: 'availableBeds' },
{ text: '床位利用率', value: 'bedusagerate' }
],
startDate: null,
endDate: null,
menu1: false,
menu2: false,
bedcount: 0,
roomcount: 0,
guadanStatistics: {
guadanTotalCount: 0,
guadanCurrentCount: 0,
guadanPeopleTotal: 0,
guadanPeopleMale: 0,
guadanPeopleFemale: 0,
guadanPeopleCurrent: 0,
guadanPeopleCurrentMale: 0,
guadanPeopleCurrentFemale: 0
},
bookerDialog: {
title: "",
show: false,
items: [],
headers: [
{ text: '姓名', value: 'name' },
{ text: "性别", value: 'gender'}
]
}
}
},
methods: {
showBookDialog(item, title, type = null) {
this.bookerDialog.title = title;
this.bookerDialog.show = true;
if (type === 'booking') {
this.getBookerDialogBookingItems(item.date);
}
else if (type === 'checkin') {
this.getBookerDialogCheckInItems(item.date);
}
},
closeBookDialog() {
this.bookerDialog.show = false;
this.bookerDialog.title = "";
this.bookerDialog.items = [];
},
async getBookerDialogBookingItems(date) {
try {
const res = await axios.get(HTTP_HOST + 'api/guadan/guest/booking/list', {
params: {
date: date
}
});
this.bookerDialog.items = res.data;
} catch(error) {
}
},
async getBookerDialogCheckInItems(date) {
try {
const res = await axios.get(HTTP_HOST + 'api/guadan/guest/checkin/list', {
params: {
date: date
}
});
this.bookerDialog.items = res.data;
} catch (error) {
}
},
async getList() {
try {
const res = await axios.get(HTTP_HOST + 'api/guadan/guadanstatisticstable/list', {
params: {
start: this.startDate || '',
end: this.endDate || ''
}
});
this.items = res.data.statistics;
this.roomcount = res.data.roomcount;
this.bedcount = res.data.bedcount;
} catch (e) {
console.error(e);
}
},
exportStatisticsToExcel() {
if (!this.items || !this.items.length) {
console.warn("没有数据可导出");
return;
}
// 1. 取 items 数组并格式化
const sheetData = this.items.map(item => ({
日期: item.date.split('T')[0], // 格式化成 YYYY-MM-DD
预订人数: item.todaytotalbookers,
入住人数: item.checkin
}));
// 2. 转换成 XLSX Sheet
const ws = XLSX.utils.json_to_sheet(sheetData);
// 3. 创建 Workbook 并添加 Sheet
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "统计数据");
// 4. 写入 Excel 并下载
const wbout = XLSX.write(wb, { bookType: "xlsx", type: "array" });
saveAs(new Blob([wbout], { type: "application/octet-stream" }), "statistics.xlsx");
},
GetGuadanStatistics() {
axios.get(HTTP_HOST + 'api/guadanStatistics/GetGuadanStatistics')
.then((res) => {
this.guadanStatistics = res.data.guadanStatistics;
})
}
},
mounted() {
this.getList();
this.GetGuadanStatistics();
}
})
</script>
<!-- CDN 方式引入 XLSX 和 FileSaver -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
</asp:Content>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class admin_guadan_statistics_table : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{
}
}

539
web/admin/guadan/view.aspx Normal file
View File

@@ -0,0 +1,539 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="view.aspx.cs" Inherits="admin_guadan_view" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" Runat="Server">
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<fieldset class="border rounded p-4 mb-5 shadow-sm bg-white">
<legend class="w-auto px-3 font-weight-bold text-primary">掛單資訊</legend>
<!-- 🟢 區塊一:掛單資訊 -->
<div class="border rounded p-3 bg-white shadow-sm" style="pointer-events: none; user-select: none; background: #1c5bd9; padding: 5px;">
<h6 class="text-secondary mb-3">📝掛單資訊</h6>
<div class="form-group row mt-3">
<label class="col-sm-2 col-form-label text-center">掛單單號(不可修改)</label>
<div class="col-sm-4 text-left">
<input class="form-control" v-model="guadanorder.order_form.orderNo" readonly />
</div>
<label class="col-sm-2 col-form-label text-center">關聯活動</label>
<div class="col-sm-4">
<select class="form-control" v-model="guadanorder.order_form.activityNum" >
<option :value="null">未關聯</option>
<option v-for="activity in activityList" :key="activity.num" :value="activity.num">
{{activity.subject}}
</option>
</select>
</div>
</div>
<div class="form-group row mt-3">
<label class="col-sm-2 col-form-label text-center">
預約開始日期
</label>
<div class="col-sm-4 text-left">
<input class="form-control" type="date" v-model="guadanorder.order_form.startdate" />
</div>
<label class="col-sm-2 col-form-label text-center">
預約結束日期
</label>
<div class="col-sm-4">
<input class="form-control" type="date" v-model="guadanorder.order_form.enddate" />
</div>
</div>
<div class="form-group row mt-3">
<label class="col-sm-2 col-form-label text-center">預定人姓名</label>
<div class="col-sm-4">
<input class="form-control" v-model="guadanorder.order_form.bookerName" />
</div>
<label class="col-sm-2 col-form-label text-center">預定人電話</label>
<div class="col-sm-4">
<input class="form-control" v-model="guadanorder.order_form.bookerPhone" />
</div>
</div>
<div class="form-group row mt-3">
<label class="col-sm-2 col-form-label text-center">備註</label>
<div class="col-sm-4">
<textarea class="form-control" v-model="guadanorder.order_form.note"></textarea>
</div>
</div>
</div>
</fieldset>
<fieldset class="border rounded p-4 mb-5 shadow-sm bg-white">
<!-- 表格標題緊貼表格上方,居中 -->
<div class="d-flex align-items-center mb-3">
<!-- 中間標題flex-grow撐開居中 -->
<div class="flex-grow-1 text-center">
<h5 class="text-primary fw-bold mb-0">
<i class="bi bi-people-fill me-2"></i>掛單蓮友
</h5>
</div>
</div>
<!-- v-data-table 表格 -->
<v-data-table :headers="guadanguest.headers" :items="guadanguest.items" class="elevation-1 rounded" dense>
<template #item.checkinat="{item}">
{{item.checkinat |timeString('YYYY-MM-DD')}}
</template>
<template #item.checkoutat="{item}">
{{item.checkoutat |timeString('YYYY-MM-DD')}}
</template>
<template v-slot:item.name="{item}">
{{item.follower?.u_name}}
</template>
<template v-slot:item.sex="{item}">
{{item.follower?.sex}}
</template>
</v-data-table>
</fieldset>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" Runat="Server">
<style>
/* 調整 fieldset 風格 */
fieldset {
border: 1px solid #dee2e6;
border-radius: 0.5rem;
background-color: #fff;
}
legend {
font-size: 1.25rem;
font-weight: 700;
color: #0d6efd;
width: auto;
padding: 0 0.75rem;
}
.form-group label {
font-weight: 600;
}
/* 按鈕置右 */
.text-right {
text-align: right;
}
/* 選擇床位相關 */
.tree,
.tree ul {
list-style: none;
margin: 0;
padding-left: 1rem;
}
.toggle-icon {
cursor: pointer;
user-select: none;
width: 1rem;
display: inline-block;
color: #007bff;
}
.region-item-label {
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
display: inline-block;
}
.region-item-label.selected {
background-color: #eaf4ff;
color: #0d6efd;
font-weight: bold;
}
.selected-room {
background-color: #cce5ff;
font-weight: bold;
}
/* 選擇床位相關 */
</style>
<script>
Vue.component('region-item', {
props: ['item', 'selectedId', 'selectedType', 'expandAll', 'collapseAll'],
data() {
return {
expanded: false, // 預設全部收起
selectedRoomId: null,
}
},
watch: {
expandAll(newVal) {
if (newVal) {
this.expanded = true;
// 執行完後發事件通知父組件清除標誌
this.$nextTick(() => this.$emit('clear-expand-all'));
}
},
collapseAll(newVal) {
if (newVal) {
this.expanded = false;
this.$nextTick(() => this.$emit('clear-collapse-all'));
}
}
},
computed: {
hasChildren() {
return this.item.children && this.item.children.length > 0;
},
icon() {
// 無論有無子節點,皆可點擊展開/收起
return this.expanded ? '▼' : '▶';
},
isSelected() {
return this.selectedType === 'region' && this.item.uuid === this.selectedId;
}
},
methods: {
toggle() {
this.expanded = !this.expanded;
},
select() {
this.$emit('select-region', this.item);
},
selectRoom(room) {
this.selectedRoomId = room.uuid;
this.$emit('select-room', room); // 可以發事件給父組件
}
},
template: `
<div>
<span class="toggle-icon" @click="toggle">{{ icon }}</span>
<span @click="select"
class="region-item-label"
:class="{ 'selected': isSelected }">
{{ item.rooms.length>0 ? (item.name + '(' + item.rooms.length + '房)'): item.name }}
</span>
<!-- 子區域列表 -->
<ul v-if="hasChildren && expanded">
<li v-for="(child, index) in item.children" :key="child.id + '-' + index">
<region-item
:item="child"
:selected-id="selectedId"
:selected-type="selectedType"
:expand-all="expandAll"
:collapse-all="collapseAll"
@select-region="$emit('select-region', $event)"
@select-room="$emit('select-room', $event)"
@clear-expand-all="$emit('clear-expand-all')"
@clear-collapse-all="$emit('clear-collapse-all')"
/>
</li>
</ul>
<!-- 客房列表:無論是否有子區域,只要展開就顯示 -->
<ul v-if="item.rooms && item.rooms.length > 0 && expanded">
<li v-for="room in item.rooms" :key="'room-' + room.uuid"
@click="selectRoom(room)"
:class="{ 'selected-room': selectedType === 'room' && selectedId === room.uuid }"
style="cursor: pointer;">
<span class="bed-label">
🛏️ {{ room.name + ' (' + room.beds.length + '床) ' + (room.gender === true ? '(男客房)' : room.gender === false ? '(女客房)' : '') }}
</span>
</li>
</ul>
</div>
`
});
Vue.filter('timeString', function (value, myFormat) {
return value == null || value == "" ? "" : moment(value).format(myFormat || 'YYYY-MM-DD, HH:mm:ss');
});
new Vue({
el: '#app',
vuetify: new Vuetify(vuetify_options),
data() {
return {
activityList: [],
availableBedCount: {
male: 0,
female: 0,
},
guadanorder: {
order_form: {
uuid: '<%= Request.QueryString["orderid"] %>' || null,
startdate: null,
enddate: null,
note: null,
orderNo: null,
bookerName: null,
bookerPhone: null,
bookerFollowerNum: null,
activityNum: null,
},
status_items: [],
},
guadanguest: {
guest: {
uuid: null, // int?
fullName: '', // string
gender: null, // int?
phone: '', // string
idNumber: '', // string
birthday: null, // Date (建議用 date picker)
email: '', // string
address: '', // string
emergencyContact: '', // string
emergencyPhone: '', // string
status: null, // int?
notes: '' // string
},
headers: [{
text: '姓名',
value: 'name'
},
{
text: '性別',
value: 'sex'
},
{
text: '掛單開始時間',
value: 'checkinat'
},
{
text: '掛單結束時間',
value: 'checkoutat'
},
{
text: '床位',
value: 'bedName'
},
{
text: '狀態',
value: 'statusName'
},
{
text: '備註',
value: 'note'
},
{
text: '',
value: 'actions'
},
],
items: [],
showCreateGuestModal: false,
xuzhu: {
showXuzhuGuestModal: false,
currentCheckoutDate: null,
newCheckoutDate: null,
guestUuid: null,
guestBedUuid: null,
}
},
checkInGuest: {
showSelectGuadanOrderGuest: false,
isEdit: false,
inGuest: {
uuid: null,
orderNo: null,
followerNum: null,
roomUuid: null,
bedUuid: null,
checkInAt: null,
checkOutAt: null,
statuscode: null,
},
status: [],
},
region_modal: {
regions: [],
currentSelectRegion: null,
currentSelectRoom: null,
currentSelectBeds: [],
currentSelectBed: null,
showSelectBedModal: false,
selectedId: null, // 被選中項目ID
selectedType: null, // 'region' 或 'room'
expandAllFlag: false, // 控制全部展開
collapseAllFlag: false, // 控制全部收起
currentSelectBedText: null,
},
selectGuestModal: {
showSelectGuestModal: false,
currentSelectedGuest: null,
fullNameText: null,
headers: [{
text: '姓名',
value: 'u_name'
},
{
text: '電話',
value: 'phone'
},
{
text: '',
value: 'actions'
},
],
items: [],
options: { //v-data-table參數
page: 1,
itemsPerPage: 10,
sortBy: [],
sortDesc: [],
multiSort: false,
},
page: 1,
pageSize: 10,
count: 0,
footer: {
showFirstLastPage: true,
disableItemsPerPage: true,
itemsPerPageAllText: '',
itemsPerPageText: '',
},
searchNameOrPhone: null,
},
automaticBedAllocation: {
showModal: false,
// 蓮友選擇彈出視窗
followerModal: {
showModal: false,
followerList: [],
selectedFollowerItems: [],
page: 1,
pageSize: 10,
totalCount: 0,
searchNameOrPhone: '',
headers: [
{ text: '姓名', value: 'u_name' },
{ text: '電話', value: 'phone' },
{ text: '操作', value: 'actions', sortable: false }
],
},
// 已選擇的待分配列表
selectedFollowers: [],
preBeds: [],
headers: [
{ text: '姓名', value: 'u_name' },
{ text: '電話', value: 'phone' },
{ text: '性別', value: 'sex' },
{ text: '預分配床位', value: 'prebed' },
{ text: '', value: 'actions' }
],
},
}
},
methods: {
getActivityList() {
axios.post(HTTP_HOST + 'api/activity/GetList?page=1&pageSize=500', { kind: 0, subject: "" })
.then((res) => {
this.activityList = res.data.list
})
},
//掛單相關方法-------------------start
validateOrderForm() {
if (!this.guadanorder.order_form.startdate) {
this.$refs.messageModal.open({
message: '請輸入必填資訊'
});
return false;
}
if (!this.guadanorder.order_form.enddate) {
this.$refs.messageModal.open({
message: '請輸入必填資訊'
});
return false;
}
if (!this.guadanorder.order_form.bookerName) {
this.$refs.messageModal.open({
message: '請輸入姓名'
});
return false;
}
if (this.guadanorder.order_form.bookerPhone && !/^\d{2,4}-?\d{3,4}-?\d{3,4}$/.test(this.guadanorder.order_form.bookerPhone)) {
this.$refs.messageModal.open({
message: '電話輸入有誤'
});
return false;
}
return true;
},
getGuadanOrderById() {
if (this.guadanorder.order_form.uuid) {
axios.get(HTTP_HOST + 'api/guadan/getorderbyid', {
params: {
orderId: this.guadanorder.order_form.uuid
}
}).then((res) => {
this.guadanorder.order_form.note = res.data.notes;
this.guadanorder.order_form.startdate = res.data.startDate;
this.guadanorder.order_form.enddate = res.data.endDate;
this.guadanorder.order_form.orderNo = res.data.guaDanOrderNo;
this.guadanorder.order_form.bookerName = res.data.bookerName;
this.guadanorder.order_form.bookerPhone = res.data.bookerPhone;
this.guadanorder.order_form.bookerFollowerNum = res.data.bookerFollowerNum;
this.guadanorder.order_form.uuid = res.data.uuid;
this.guadanorder.order_form.activityNum = res.data.activityNum;
})
}
},
getGuadanOrderGuestByOrderNo() {
if (this.guadanorder.order_form.orderNo) {
axios.get(HTTP_HOST + 'api/guadanorderguest/getbyorderno', {
params: {
orderNo: this.guadanorder.order_form.orderNo
}
}).then((res => {
this.guadanguest.items = res.data;
}))
}
},
//掛單相關方法-------------------end
},
watch: {
'guadanorder.order_form.orderNo'(newValue, oldValue) {
if (newValue) {
this.getGuadanOrderGuestByOrderNo();
}
},
'selectGuestModal.options': {
handler() {
this.getGuadanFollowers();
},
deep: true
},
// 分頁變化時自動刷新
'automaticBedAllocation.followerModal.page': function () {
this.getMultiSelectFollowers();
},
'automaticBedAllocation.followerModal.pageSize': function () {
this.getMultiSelectFollowers();
}
},
mounted() {
if (this.guadanorder.order_form.uuid) {
this.getGuadanOrderById();
this.getGuadanOrderGuestByOrderNo();
}
this.getActivityList();
},
computed: {
pageCount() {
return Math.ceil(this.selectGuestModal.count / this.selectGuestModal.pageSize)
},
pageCount2: function () {
var fm = this.automaticBedAllocation.followerModal;
return Math.ceil(fm.totalCount / fm.pageSize) || 1;
}
},
});
</script>
</asp:Content>

View File

@@ -5,7 +5,7 @@ using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class admin_guadan_statistics : MyWeb.config
public partial class admin_guadan_view : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{

File diff suppressed because it is too large Load Diff

768
web/admin/pivot/README.md Normal file
View File

@@ -0,0 +1,768 @@
# Pivot Module 執行計劃
## 概述
建立多頁籤數據透視查詢模組,參考 transfer 模組的設計架構,使用相同的技術棧與 UI/UX 模式,實現法會報名資料的多維度分析與展示。
---
## 技術架構
### 前端技術
- **框架**: Vue.js 2.x + Vuetify 2.x與 transfer 模組一致)
- **表格元件**: v-data-tableVuetify 內建表格元件)
- **頁籤元件**: v-tabs / v-tab / v-tab-itemVuetify 頁籤)
- **UI 框架**: Bootstrap 5 + Bootstrap IconsMasterPage
- **樣式**: 與 transfer 模組保持一致的視覺風格
### 後端 API
- **框架**: ASP.NET Web APIC#
- **ORM**: Entity Framework + LINQ
- **控制器**: `App_Code/api/pivotController.cs`(已建立)
- **資料庫視圖**: `報名明細查詢`SQL View
### 資料流架構(重要)★
```
查詢流程:
┌─────────────┐
│ Tab 1 │ → 選擇法會 → API 查詢一次 →
│ 查詢條件 │ (完整資料集)
└─────────────┘
┌─────────────────────────────────────────┐
│ Vue Data (存於前端 this.rawData) │
│ - 完整報名明細 │
│ - 一次性載入,不重複查詢 │
└─────────────────────────────────────────┘
┌──────┬──────┬──────┬──────┬──────┐
│Tab 2 │Tab 3 │Tab 4 │Tab 5 │Tab 6 │
│明細 │信眾 │收入 │趨勢 │對比 │
│ │ │ │ │ │
│ 純前端計算 / 過濾 / 分組 / 統計 │
│ 使用 computed / methods / filters │
└──────────────────────────────────────┘
```
**優點**:
1.**效能優化**: API 只查詢一次,減少伺服器負載
2.**即時響應**: 切換頁籤無延遲,使用者體驗佳
3.**離線分析**: 資料載入後可離線操作(過濾、排序、統計)
4.**減少流量**: 不重複傳輸相同資料
5.**一致性**: 所有頁籤基於同一份資料,確保一致性
**技術實現**:
- `this.rawData`: 原始完整資料Tab 1 查詢後存入)
- `computed properties`: 各頁籤的資料來源(動態計算)
- `methods`: 過濾、分組、統計邏輯
- `watch`: 監聽過濾條件變化
### 元件規格
1. **日期選擇器**: `v-date-picker``<input type="date">`
2. **下拉選單**: `v-select`(年份、月份、法會選擇)
3. **資料表格**: `v-data-table`(分頁、排序、過濾)
4. **頁籤切換**: `v-tabs`Tab 1~N
5. **按鈕群組**: `v-btn`(查詢、匯出、重設)
6. **載入狀態**: `:loading="loading"`
7. **視覺標記**: Bootstrap Badge橙、藍、綠、紫色標籤
---
## 頁籤設計
### Tab 1: 查詢條件設定
**功能目標**: 提供查詢條件,篩選法會並選擇目標法會
#### UI 布局
```
+--------------------------------------------------------------+
| [查詢條件] |
|--------------------------------------------------------------|
| 時間範圍: [年份 ▼] [月份 ▼] [查詢法會] |
|--------------------------------------------------------------|
| 法會清單: |
| +----------------------------------------------------------+ |
| | 序號 | 法會名稱 | 開始日期 | 結束日期 | 報名人數 | 操作 | |
| +----------------------------------------------------------+ |
| | 1 | 2025春季法會 | 2025-03-01 | 2025-03-15 | 120 | [選擇] | |
| | 2 | 2025夏季法會 | 2025-06-01 | 2025-06-20 | 85 | [選擇] | |
| +----------------------------------------------------------+ |
+--------------------------------------------------------------+
```
#### 查詢流程
1. 選擇年份2020~2025或月份1~12
2. 點擊「查詢法會」按鈕
3. API 回傳該期間的法會清單(`api/pivot/activity_stats`
4. 表格呈現法會清單(法會名稱、日期、統計)
5. 點擊「選擇」按鈕,載入該法會的詳細資料
6. 自動切換到 Tab 2詳細資料頁籤
#### API 整合
- **端點**: `GET api/pivot/activity_stats?startDate={start}&endDate={end}`
- **回傳**: 法會清單(含報名統計)
#### 資料查詢策略
```javascript
// 選擇法會後,一次性載入完整資料
selectActivity(item) {
this.loading = true;
this.selectedActivity = item;
// 一次性查詢完整報名明細(不分頁)
axios.get('/api/pivot/registration_details', {
params: {
activityNum: item.法會ID,
pageSize: 9999 // 取得全部資料
}
}).then(response => {
// 存入原始資料(供所有頁籤使用)
this.rawData = response.data.data.list;
// 自動切換到 Tab 2
this.activeTab = 1;
this.loading = false;
});
}
```
#### 元件範例
```html
<v-select
:items="yearOptions"
v-model="selectedYear"
label="年份"
dense
outlined
></v-select>
<v-select
:items="monthOptions"
v-model="selectedMonth"
label="月份"
dense
outlined
clearable
></v-select>
<v-btn color="primary" @click="loadActivities">查詢法會</v-btn>
<v-data-table
:headers="activityHeaders"
:items="activities"
:loading="loading"
item-key="法會ID"
>
<template v-slot:item.actions="{ item }">
<v-btn small color="success" @click="selectActivity(item)">選擇</v-btn>
</template>
</v-data-table>
```
---
### Tab 2: 報名明細資料
**功能目標**: 完整呈現該場法會的所有報名明細
#### UI 布局
```
+--------------------------------------------------------------+
| [報名明細] 法會: 2025春季法會 (2025-03-01 ~ 2025-03-15) |
|--------------------------------------------------------------|
| 過濾: [信眾姓名] [功德類型 ▼] [狀態 ▼] [查詢] [匯出Excel] |
|--------------------------------------------------------------|
| +----------------------------------------------------------+ |
| | 報名編號 | 報名日期 | 信眾姓名 | 功德名稱 | 數量 | 金額 | |
| +----------------------------------------------------------+ |
| | 20250301001 | 2025-03-01 | 張三 | 點燈 | 1 | 500 | |
| | 20250301002 | 2025-03-01 | 李四 | 安太歲 | 1 | 300 | |
| +----------------------------------------------------------+ |
| 第 1 頁,共 10 頁(共 200 筆) |
+--------------------------------------------------------------+
```
#### 功能特性
1. **欄位顯示**: 報名編號、報名日期、信眾姓名、功德名稱、數量、金額、已收、未收
2. **過濾條件**: 信眾姓名(模糊搜尋)、功德類型(下拉選單)、功德主(是/否)
3. **排序**: 可依任意欄位排序(升序/降序)
4. **分頁**: 預設每頁 50 筆可調整10/20/50/100
5. **匯出**: 匯出 Excel/CSV
#### 欄位色彩標記(參考 Excel 視圖)
- **橙色(法會資料)**: 法會ID、法會名稱、開始日期、結束日期
- **藍色(信眾資料)**: 信眾編號、信眾姓名
- **綠色(功德資訊)**: 報名編號、報名日期、功德主、功德類型、功德名稱
- **紫色(計算欄位)**: 數量、金額、已收、未收
使用 Bootstrap Badge 或背景色區隔:
```html
<span class="badge bg-warning text-dark">法會</span>
<span class="badge bg-info">信眾</span>
<span class="badge bg-success">功德</span>
<span class="badge bg-secondary">計算</span>
```
#### 資料來源(前端計算)
```javascript
computed: {
// Tab 2: 報名明細(前端分頁、過濾)
filteredRegistrations() {
let data = this.rawData;
// 過濾:信眾姓名
if (this.filter.followerName) {
data = data.filter(x => x.信眾姓名.includes(this.filter.followerName));
}
// 過濾:功德類型
if (this.filter.itemKind) {
data = data.filter(x => x.功德類型 === this.filter.itemKind);
}
// 過濾:功德主
if (this.filter.isParent !== null) {
data = data.filter(x => x.功德主 === (this.filter.isParent ? '是' : '否'));
}
return data;
},
// 前端分頁
paginatedRegistrations() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.filteredRegistrations.slice(start, end);
}
}
```
#### 元件範例
```html
<v-data-table
:headers="detailHeaders"
:items="registrations"
:loading="loading"
:server-items-length="totalCount"
:options.sync="options"
item-key="報名編號"
class="elevation-1"
>
<template v-slot:item.信眾姓名="{ item }">
<span class="badge bg-info me-1"></span>{{ item.信眾姓名 }}
</template>
<template v-slot:item.金額="{ item }">
<span class="badge bg-secondary me-1"></span>{{ item.金額 | currency }}
</template>
</v-data-table>
```
---
### Tab 3: 信眾參與分析
**功能目標**: 統計信眾的參與情況(參與次數、金額、最近參與)
#### UI 布局
```
+--------------------------------------------------------------+
| [信眾參與分析] |
|--------------------------------------------------------------|
| 過濾: [信眾編號] [參與次數 ≥] [總金額 ≥] [查詢] |
|--------------------------------------------------------------|
| +----------------------------------------------------------+ |
| | 信眾編號 | 姓名 | 參與次數 | 總金額 | 最近參與日期 | |
| +----------------------------------------------------------+ |
| | F001 | 張三 | 5 | 2500 | 2025-03-01 | |
| | F002 | 李四 | 3 | 1500 | 2025-02-15 | |
| +----------------------------------------------------------+ |
+--------------------------------------------------------------+
```
#### 功能特性
1. **統計欄位**: 參與次數、總金額、平均金額、最近參與日期
2. **過濾條件**: 信眾編號、參與次數閾值、總金額閾值
3. **排序**: 預設依參與次數降序
4. **分頁**: 預設每頁 50 筆
#### 資料來源(前端計算)
```javascript
computed: {
// Tab 3: 信眾參與分析(從 rawData 計算)
followerAnalysis() {
const followerMap = {};
this.rawData.forEach(item => {
const fNum = item.信眾編號;
if (!followerMap[fNum]) {
followerMap[fNum] = {
信眾編號: fNum,
姓名: item.信眾姓名,
參與次數: 0,
總金額: 0,
最近參與日期: item.報名日期
};
}
followerMap[fNum].參與次數++;
followerMap[fNum].總金額 += (item.金額 * item.數量);
// 更新最近參與日期
if (new Date(item.報名日期) > new Date(followerMap[fNum].最近參與日期)) {
followerMap[fNum].最近參與日期 = item.報名日期;
}
});
// 轉換為陣列,並計算平均金額
return Object.values(followerMap).map(f => ({
...f,
平均金額: Math.round(f.總金額 / f.參與次數)
})).sort((a, b) => b.參與次數 - a.參與次數); // 依參與次數降序
}
}
```
---
### Tab 4: 收入統計分析
**功能目標**: 依時間、法會、功德類型統計收入
#### UI 布局
```
+--------------------------------------------------------------+
| [收入統計分析] |
|--------------------------------------------------------------|
| 分組方式: ( ) 月份 ( ) 年度 (•) 法會 ( ) 功德類型 |
|--------------------------------------------------------------|
| +----------------------------------------------------------+ |
| | 分組名稱 | 報名人數 | 總金額 | 已收 | 未收 | 收款率 | |
| +----------------------------------------------------------+ |
| | 2025春季法會 | 120 | 60000 | 50000 | 10000 | 83.3% | |
| | 2025夏季法會 | 85 | 45000 | 40000 | 5000 | 88.9% | |
| +----------------------------------------------------------+ |
+--------------------------------------------------------------+
```
#### 功能特性
1. **分組方式**: 月份、年度、法會、功德類型
2. **統計欄位**: 報名人數、總金額、已收、未收、收款率
3. **圖表呈現**: 可加入 CanvasJS 長條圖/圓餅圖(選配)
4. **匯出**: 支援 Excel/CSV
#### 資料來源(前端計算)
```javascript
computed: {
// Tab 4: 收入統計分析(從 rawData 計算)
incomeStats() {
const statsMap = {};
const groupBy = this.groupBy; // 'monthly', 'yearly', 'activity', 'itemKind'
this.rawData.forEach(item => {
let key;
switch(groupBy) {
case 'monthly':
key = item.報名日期.substring(0, 7); // YYYY-MM
break;
case 'yearly':
key = item.報名日期.substring(0, 4); // YYYY
break;
case 'activity':
key = item.法會名稱;
break;
case 'itemKind':
key = item.功德類型;
break;
}
if (!statsMap[key]) {
statsMap[key] = {
分組名稱: key,
報名人數: 0,
總金額: 0,
已收: 0,
未收: 0
};
}
statsMap[key].報名人數++;
const amount = item.金額 * item.數量;
statsMap[key].總金額 += amount;
statsMap[key].已收 += item.已收 || 0;
statsMap[key].未收 += item.未收 || amount;
});
// 計算收款率
return Object.values(statsMap).map(s => ({
...s,
收款率: s.總金額 > 0 ? ((s.已收 / s.總金額) * 100).toFixed(1) + '%' : '0%'
}));
}
}
```
---
### Tab 5: 趨勢分析
**功能目標**: 顯示時間序列趨勢(收入、參與人數、法會數量)
#### UI 布局
```
+--------------------------------------------------------------+
| [趨勢分析] |
|--------------------------------------------------------------|
| 指標: ( ) 收入 (•) 參與人數 ( ) 法會數量 |
| 時間間隔: ( ) 月份 (•) 季度 ( ) 年度 |
|--------------------------------------------------------------|
| [折線圖] |
| |
| +----------------------------------------------------------+ |
| | 時間 | 數值 | 成長率 | |
| +----------------------------------------------------------+ |
| | 2025-01 | 120 | +10% | |
| | 2025-02 | 132 | +10% | |
| +----------------------------------------------------------+ |
+--------------------------------------------------------------+
```
#### 功能特性
1. **指標選擇**: 收入、參與人數、法會數量
2. **時間間隔**: 月份、季度、年度
3. **成長率計算**: 較前期成長率
4. **圖表**: CanvasJS 折線圖(選配)
#### 資料來源(前端計算)
```javascript
computed: {
// Tab 5: 趨勢分析(從 rawData 計算)
trendAnalysis() {
const metric = this.trendMetric; // 'income', 'followers', 'count'
const interval = this.trendInterval; // 'monthly', 'quarterly', 'yearly'
const trendMap = {};
this.rawData.forEach(item => {
let key;
switch(interval) {
case 'monthly':
key = item.報名日期.substring(0, 7); // YYYY-MM
break;
case 'quarterly':
const month = parseInt(item.報名日期.substring(5, 7));
const quarter = Math.ceil(month / 3);
key = `${item.報名日期.substring(0, 4)}-Q${quarter}`;
break;
case 'yearly':
key = item.報名日期.substring(0, 4); // YYYY
break;
}
if (!trendMap[key]) {
trendMap[key] = { 時間: key, 收入: 0, 人數: 0, 次數: 0 };
}
trendMap[key].收入 += (item.金額 * item.數量);
trendMap[key].人數++; // 簡化計算,實際可用 Set 去重
trendMap[key].次數++;
});
// 轉換為陣列並排序
const result = Object.values(trendMap).sort((a, b) => a.時間.localeCompare(b.時間));
// 計算成長率
return result.map((item, index) => {
if (index === 0) {
return { ...item, 數值: item[metric === 'income' ? '收入' : metric === 'followers' ? '人數' : '次數'], 成長率: '-' };
}
const prev = result[index - 1];
const currentValue = item[metric === 'income' ? '收入' : metric === 'followers' ? '人數' : '次數'];
const prevValue = prev[metric === 'income' ? '收入' : metric === 'followers' ? '人數' : '次數'];
const growthRate = prevValue > 0 ? (((currentValue - prevValue) / prevValue) * 100).toFixed(1) : '0';
return { ...item, 數值: currentValue, 成長率: growthRate + '%' };
});
}
}
```
---
### Tab 6: 對比分析(選配)
**功能目標**: 不同時期、法會的對比分析
#### UI 布局
```
+--------------------------------------------------------------+
| [對比分析] |
|--------------------------------------------------------------|
| 對比類型: (•) 法會對比 ( ) 年度對比 |
| 期間1: [2025春季法會 ▼] 期間2: [2024春季法會 ▼] |
|--------------------------------------------------------------|
| +----------------------------------------------------------+ |
| | 指標 | 期間1 | 期間2 | 差異 | 差異率 | |
| +----------------------------------------------------------+ |
| | 報名人數 | 120 | 100 | +20 | +20% | |
| | 總金額 | 60000 | 50000 | +10000 | +20% | |
| +----------------------------------------------------------+ |
+--------------------------------------------------------------+
```
#### 資料來源(前端計算)
```javascript
computed: {
// Tab 6: 對比分析(從 rawData 計算)
// 注意:對比分析可能需要跨法會資料,如需要可額外查詢
comparativeAnalysis() {
// 如果只在單一法會內對比(如月份對比),可用 rawData
// 如果需要跨法會對比,建議另外查詢或在 Tab 1 時一併載入多場法會資料
const period1Data = this.rawData.filter(x => {
// 依 period1 條件過濾
return this.isPeriod1(x);
});
const period2Data = this.rawData.filter(x => {
// 依 period2 條件過濾
return this.isPeriod2(x);
});
const stats1 = this.calculateStats(period1Data);
const stats2 = this.calculateStats(period2Data);
return [
{ 指標: '報名人數', 期間1: stats1.count, 期間2: stats2.count, 差異: stats1.count - stats2.count },
{ 指標: '總金額', 期間1: stats1.amount, 期間2: stats2.amount, 差異: stats1.amount - stats2.amount },
// ... 其他指標
];
}
}
```
---
## 視覺設計規範
### 色彩系統(參考 transfer
- **主色調**: Bootstrap 5 預設配色
- **成功/確認**: `bg-success` / `text-success`
- **警告**: `bg-warning` / `text-warning`
- **危險/錯誤**: `bg-danger` / `text-danger`
- **資訊**: `bg-info` / `text-info`
- **次要**: `bg-secondary` / `text-muted`
### 欄位色彩標記(對應 Excel
```html
<!-- 橙色:法會資料 -->
<span class="badge bg-warning text-dark"></span>
<!-- 藍色:信眾資料 -->
<span class="badge bg-info"></span>
<!-- 綠色:功德資訊 -->
<span class="badge bg-success">功德</span>
<!-- 紫色:計算欄位 -->
<span class="badge bg-secondary"></span>
```
### 表格樣式
```css
/* 與 transfer 保持一致 */
.v-data-table th {
background-color: #f5f5f5 !important;
font-weight: bold;
}
.v-data-table tbody tr:hover {
background-color: #f0f8ff !important;
}
/* 欄位色彩標記 */
.field-activity { border-left: 3px solid #ffc107; } /* 橙 */
.field-follower { border-left: 3px solid #17a2b8; } /* 藍 */
.field-merit { border-left: 3px solid #28a745; } /* 綠 */
.field-calculated { border-left: 3px solid #6c757d; } /* 紫 */
```
---
## API 端點總覽
| 端點 | 方法 | 功能 | 對應頁籤 | 使用時機 |
|------|------|------|----------|----------|
| `/api/pivot/activity_stats` | GET | 查詢法會清單與統計 | Tab 1 | 選擇查詢條件時 |
| `/api/pivot/registration_details` | GET | 報名明細查詢(**完整資料** | Tab 1 | **選擇法會後一次性載入** ★ |
| `/api/pivot/registration_details_export` | GET | 報名明細匯出 | Tab 2 | 匯出 Excel 時(選配) |
| `/api/pivot/excel_data_structured` | GET | Excel 數據連接 | 外部 | Power Query/Power Pivot |
**重要說明**:
-**Tab 2~6 不呼叫 API**,全部使用前端 `computed` 計算
-`registration_details` 查詢時使用 `pageSize=9999` 取得完整資料
- ✅ 原 `follower_analysis`, `income_stats`, `trend_analysis`, `comparative_analysis` 端點**保留備用**,但前端優先使用 computed
- ✅ 如資料量過大(>5000 筆),可調整策略改用分頁查詢
---
## 檔案結構
```
admin/pivot/
├── index.aspx # 首頁(已完成)
├── index.aspx.cs # 首頁邏輯(已完成)
├── query.aspx # 多頁籤查詢頁面(待建立)★
├── query.aspx.cs # 查詢頁面邏輯(待建立)★
└── README.md # 本文件
App_Code/api/
└── pivotController.cs # API 控制器(已完成)
```
---
## 實作步驟
### Phase 1: 建立查詢頁面骨架
1. ✅ 建立 `query.aspx`(參考 transfer/verify.aspx
2. ✅ 建立 `query.aspx.cs`(參考 transfer/verify.aspx.cs
3. ✅ 引入 Vue.js + Vuetify
4. ✅ 建立頁籤結構v-tabs
### Phase 2: 實作 Tab 1查詢條件
1. ✅ 年份/月份選擇器
2. ✅ 查詢法會按鈕與 API 整合(`activity_stats`
3. ✅ 法會清單表格v-data-table
4. ✅ 選擇法會邏輯(一次性載入完整資料)
5.`this.rawData` 資料結構設計
### Phase 3: 實作 Tab 2報名明細
1. ✅ 報名明細表格(前端分頁、排序、過濾)
2.`computed: filteredRegistrations` 實作
3. ✅ 欄位色彩標記badge
4. ✅ 匯出功能(選配)
### Phase 4: 實作 Tab 3~6分析頁籤
1. ✅ 信眾參與分析Tab 3- `computed: followerAnalysis`
2. ✅ 收入統計分析Tab 4- `computed: incomeStats`
3. ✅ 趨勢分析Tab 5- `computed: trendAnalysis`
4. ✅ 對比分析Tab 6選配- `computed: comparativeAnalysis`
### Phase 5: 優化與測試
1. ✅ UI/UX 調整(與 transfer 保持一致)
2. ✅ 效能優化(分頁、快取)
3. ✅ 瀏覽器相容性測試
4. ✅ 權限控制ezAuthorize
---
## 參考範例
### 1. transfer/verify.aspx
- 多階段流程設計程序1、程序2
- v-data-table 表格元件使用
- 信眾選擇對話框v-dialog
- 狀態選擇v-select
### 2. transfer/verify1.aspx
- 單階段流程設計
- 簡潔的查詢與確認流程
### 3. transfer/index.aspx
- 功能入口頁面設計
- Bootstrap Icons + Badge
- 三欄式布局
---
## 注意事項
1. **元件一致性**: 所有元件、樣式、命名均與 transfer 模組保持一致
2. **API 規範**: 遵循 RESTful 設計,統一回傳格式
3. **權限控制**: 所有 API 加上 `[ezAuthorize]`
4. **效能考量**:
-**前端計算策略**: Tab 1 查詢一次Tab 2~6 使用 Vue computed 計算
-**資料量控制**: 單一法會報名明細預估 <5000 筆,適合前端處理
- ⚠️ **大資料處理**: 若單場法會 >5000 筆,可改用 API 分頁查詢
-**記憶體管理**: 切換法會時清除舊資料(`this.rawData = []`
5. **視覺區隔**: 使用 Badge、邊框色、背景色標記欄位類型
6. **使用者體驗**: 載入狀態loading、錯誤提示alert、空資料提示
7. **前端效能優化**:
- 使用 `computed` 而非 `methods`(自動快取)
- 大型陣列操作使用 `Object.freeze()` 凍結原始資料
- v-data-table 啟用虛擬滾動(`:virtual-scroll="true"`,資料量 >1000 時)
---
## SQL View 參考
```SQL
CREATE VIEW [dbo].[]
AS
SELECT dbo.activity.num AS ID, dbo.activity.subject AS , dbo.activity.startDate_solar AS , dbo.activity.endDate_solar AS ,
dbo.followers.f_number AS , dbo.followers.u_name AS , dbo.pro_order.order_no AS , dbo.pro_order.up_time AS ,
CASE WHEN parent_num IS NOT NULL THEN '' ELSE '' END AS , dbo.actItem_kind.kind AS , dbo.actItem.subject AS ,
dbo.pro_order_detail.qty AS , dbo.pro_order_detail.price AS , 0 AS , dbo.pro_order_detail.price * dbo.pro_order_detail.qty - 0 AS
FROM dbo.pro_order_detail INNER JOIN
dbo.pro_order ON dbo.pro_order_detail.order_no = dbo.pro_order.order_no INNER JOIN
dbo.actItem ON dbo.pro_order_detail.actItem_num = dbo.actItem.num INNER JOIN
dbo.activity ON dbo.pro_order.activity_num = dbo.activity.num INNER JOIN
dbo.followers ON dbo.pro_order.f_num = dbo.followers.num INNER JOIN
dbo.actItem_kind ON dbo.actItem.kind = dbo.actItem_kind.num
GO
```
---
## 資料結構範例
```javascript
// Vue data 結構
data() {
return {
// 原始完整資料Tab 1 查詢後存入,供所有頁籤使用)
rawData: [], // Array<報名明細物件>
// 選中的法會
selectedActivity: null,
// 當前頁籤索引
activeTab: 0, // 0=Tab1, 1=Tab2, 2=Tab3...
// Tab 2 過濾條件
filter: {
followerName: '',
itemKind: null,
isParent: null
},
// Tab 4 分組方式
groupBy: 'activity', // 'monthly', 'yearly', 'activity', 'itemKind'
// Tab 5 趨勢設定
trendMetric: 'income', // 'income', 'followers', 'count'
trendInterval: 'monthly', // 'monthly', 'quarterly', 'yearly'
// 載入狀態
loading: false
}
}
```
## Vue Computed 效能考量
```javascript
computed: {
// 使用 Object.freeze() 凍結大型陣列,提升效能
frozenRawData() {
return Object.freeze(this.rawData);
},
// 各頁籤 computed 基於 frozenRawData
filteredRegistrations() {
return this.frozenRawData.filter(/* 過濾邏輯 */);
}
}
```
## 執行確認
請檢視以上計劃,確認以下事項:
1. ✅ 頁籤設計Tab 1~6符合需求
2. ✅ 查詢流程(年/月 → 法會清單 → 明細資料)清晰
3. ✅ 視覺區隔色彩標記、Badge符合 Excel 概念
4. ✅ 技術架構Vue + Vuetify + API與 transfer 一致
5. ✅ 功能完整性(查詢、過濾、排序、匯出)
6. ✅ **資料流架構(一次查詢 + 前端計算)**合理且高效 ★
**確認後即開始實作 Phase 1建立查詢頁面骨架。**

185
web/admin/pivot/index.aspx Normal file
View File

@@ -0,0 +1,185 @@
<%@ Page Title="數據透視管理" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" EnableEventValidation="false" CodeFile="index.aspx.cs" Inherits="admin_pivot_index" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" runat="Server">
<link rel="stylesheet" href="../../js/_bootstrap-icons-1.8.1/bootstrap-icons.css">
<style>
.function-icon {
font-size: 2em;
line-height: 1;
align-content: center;
}
.external-link-icon {
font-size: 0.8em;
}
</style>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="page_nav" runat="Server">
<h2 class="mb-3">數據透視管理</h2>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
<div id="content" class="container py-4">
<div class="row">
<!-- 第一欄:報表查詢 -->
<div class="col-lg-4 mb-4">
<h5 class="text-primary mb-3">
<i class="bi bi-graph-up"></i> 報表查詢
</h5>
<div class="list-group">
<a href="activity_report.aspx" class="list-group-item list-group-item-action">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex">
<i class="bi bi-bar-chart text-success me-3 function-icon"></i>
<div>
<div>法會報名統計</div>
<small class="text-muted">各法會報名人數與金額統計</small>
</div>
</div>
<span class="badge bg-primary">管理員</span>
</div>
</a>
<a href="follower_report.aspx" class="list-group-item list-group-item-action">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex">
<i class="bi bi-people text-info me-3 function-icon"></i>
<div>
<div>信眾參與分析</div>
<small class="text-muted">信眾參與法會的詳細分析</small>
</div>
</div>
<span class="badge bg-info">管理員</span>
</div>
</a>
<a href="income_report.aspx" class="list-group-item list-group-item-action">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex">
<i class="bi bi-currency-dollar text-warning me-3 function-icon"></i>
<div>
<div>收入統計報表</div>
<small class="text-muted">各項功德金收入統計分析</small>
</div>
</div>
<span class="badge bg-warning text-dark">財務</span>
</div>
</a>
</div>
</div>
<!-- 第二欄:數據分析 -->
<div class="col-lg-4 mb-4">
<h5 class="text-primary mb-3">
<i class="bi bi-pie-chart"></i> 數據分析
</h5>
<div class="list-group">
<a href="query.aspx" class="list-group-item list-group-item-action">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex">
<i class="bi bi-diagram-3 text-primary me-3 function-icon"></i>
<div>
<div>數據透視查詢</div>
<small class="text-muted">多維度數據透視分析(完整版)</small>
</div>
</div>
<span class="badge bg-success">NEW</span>
</div>
</a>
<a href="pivot_analysis.aspx" class="list-group-item list-group-item-action">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex">
<i class="bi bi-diagram-3 text-secondary me-3 function-icon"></i>
<div>
<div>樞紐分析(舊版)</div>
<small class="text-muted">多維度數據透視分析</small>
</div>
</div>
<span class="badge bg-secondary">分析師</span>
</div>
</a>
<a href="trend_analysis.aspx" class="list-group-item list-group-item-action">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex">
<i class="bi bi-graph-up-arrow text-success me-3 function-icon"></i>
<div>
<div>趨勢分析</div>
<small class="text-muted">時間序列趨勢變化分析</small>
</div>
</div>
<span class="badge bg-success">分析師</span>
</div>
</a>
<a href="comparative_analysis.aspx" class="list-group-item list-group-item-action">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex">
<i class="bi bi-graph-down text-danger me-3 function-icon"></i>
<div>
<div>對比分析</div>
<small class="text-muted">不同時期、法會的對比分析</small>
</div>
</div>
<span class="badge bg-danger">分析師</span>
</div>
</a>
</div>
</div>
<!-- 第三欄:報表管理 -->
<div class="col-lg-4 mb-4">
<h5 class="text-primary mb-3">
<i class="bi bi-file-earmark-text"></i> 報表管理
</h5>
<div class="list-group">
<a href="custom_report.aspx" class="list-group-item list-group-item-action">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex">
<i class="bi bi-file-plus text-info me-3 function-icon"></i>
<div>
<div>自訂報表</div>
<small class="text-muted">建立自訂的報表範本</small>
</div>
</div>
<span class="badge bg-info">管理員</span>
</div>
</a>
<a href="report_schedule.aspx" class="list-group-item list-group-item-action">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex">
<i class="bi bi-clock text-secondary me-3 function-icon"></i>
<div>
<div>定期報表</div>
<small class="text-muted">設定定期自動產生報表</small>
</div>
</div>
<span class="badge bg-secondary">管理員</span>
</div>
</a>
<a href="export_center.aspx" class="list-group-item list-group-item-action">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex">
<i class="bi bi-download text-success me-3 function-icon"></i>
<div>
<div>匯出中心</div>
<small class="text-muted">報表匯出與下載管理</small>
</div>
</div>
<span class="badge bg-success">使用者</span>
</div>
</a>
</div>
</div>
</div>
<!-- 統計資訊 -->
<div class="row mt-4">
<div class="col-12">
<div class="alert alert-info">
<h6 class="alert-heading">
<i class="bi bi-info-circle"></i> 系統說明
</h6>
<ul class="mb-0">
<li><strong>報表查詢</strong>:提供各種預設報表的查詢與統計功能</li>
<li><strong>數據分析</strong>:多維度的數據透視與趨勢分析工具</li>
<li><strong>報表管理</strong>:自訂報表建立、定期報表設定與匯出管理</li>
</ul>
</div>
</div>
</div>
</div>
</asp:Content>

View File

@@ -0,0 +1,10 @@
using System;
using System.Web.UI;
public partial class admin_pivot_index : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{
// 頁面初始化(暫無邏輯)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
using System;
using System.Web.UI;
public partial class admin_pivot_pivot01 : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{
// 頁面初始化(暫無邏輯)
// 前端直接呼叫 API
}
}

1128
web/admin/pivot/query.aspx Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
using System;
using System.Web.UI;
public partial class admin_pivot_query : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{
// 頁面初始化(暫無邏輯)
}
}

View File

@@ -19,53 +19,24 @@ public partial class admin_printpw_index : MyWeb.function
private string previousOrderno = "";
protected void Page_Load(object sender, EventArgs e)
{
//Response.Write("item:"+Request["item"]+"<br>");
//Response.Write("file:"+Request["file"]+"<br>");
//Response.Write("list:"+Request["list"]+"<br>");
//if (!IsPostBack)
//{
this.Title = "預覽牌位";
if (Request.HttpMethod == "POST")
{
/*if (!String.IsNullOrEmpty(Request["item"]) &&
!String.IsNullOrEmpty(Request["file"]) &&
!String.IsNullOrEmpty(Request["list"])
)*/
if (!String.IsNullOrEmpty(Request["order_no"]))
{
var order_no = Request["order_no"];
var oderList = _db.pro_order_detail
.Where(u => u.order_no == order_no && u.printed_files != null)
.Where(u => u.order_no == order_no)
.Where(u => (u.parent_num != null)
|| u.actItem.subject.Contains("牌")
|| !string.IsNullOrEmpty(u.f_num_tablet))
//因為目前在[pro_order_detail]表中,沒有辦法區分那些項目是牌位,
//只有根據某些欄位來做部分篩選,可能會篩選錯
//但是為什麼沒有欄位可以區分一個項目是不是牌位呢?
.Select(u => new { detail_num = u.num, actItem_num = u.actItem_num }).ToArray();
Repeater1.DataSource = oderList;
Repeater1.DataBind();
}
else if (!String.IsNullOrEmpty(Request["activity_num"]))
{
//var _details = Newtonsoft.Json.JsonConvert.DeserializeObject<int[]>(Request["list"]);
//string json = "";
//using (System.IO.StreamReader oSR = new System.IO.StreamReader(Request.InputStream))
// json = oSR.ReadToEnd();
int activity_num = Convert.ToInt32(Request["activity_num"]);
//Repeater1.DataSource = _details;
var orderList = _db.pro_order.Where(u => u.activity_num == activity_num).Select(u => u.order_no).ToList();
var gdzOrderList = _db.pro_order_detail.Where(u => orderList.Contains(u.order_no) && u.print_id.Contains("主") ).Select(u=>u.order_no).Distinct().ToArray();
var datalist = _db.pro_order_detail
.Where(u => gdzOrderList.Contains(u.order_no) && u.print_id.Contains("主") && u.parent_num != null && u.printed_files != null)
.OrderBy(o => o.order_no)
.ThenBy(o => o.actItem_num)
.ThenBy(o=>o.print_id)
.Select(u => new {detail_num=u.num ,actItem_num=u.actItem_num})
.ToArray();
//List<int> ints = new List<int>();
//ints.Add(12133);
Repeater1.DataSource = datalist;
Repeater1.DataBind();
if (!String.IsNullOrEmpty(Request["title"]))
{
this.Title += " - " + Request["title"];
}
}
else
{
Response.Clear();
@@ -74,9 +45,6 @@ public partial class admin_printpw_index : MyWeb.function
}
}
//}
}
protected void Repeater1_ItemDataBound(object sender, RepeaterItemEventArgs e)
@@ -248,8 +216,8 @@ public partial class admin_printpw_index : MyWeb.function
catch (Exception ex)
{
var msg = ex.Message;
ret[0] = "??:" + msg;
ret[1] = "??";
ret[0] = "";
ret[1] = "";
}
return ret;
}

View File

@@ -24,6 +24,8 @@
@page a4l {
size: a4 landscape;
margin: 0;
margin-top:6mm;
margin-left:6mm;
}
@page a5 {
@@ -133,6 +135,22 @@ pre {
line-height: calc(var(--fs_w) * 1.0);
}
.text-block.fit-text.add-space {
letter-spacing: 0.5em;
}
.text-block.fit-text.mid_text.add-space {
padding-top: 0.5em;
}
.text-block.fit-text.mid_text.add-space.add-space-3 {
letter-spacing: 1.5em;
padding-top: 1.5em;
}
body.tblt-m .left_text {
--font-max: 30pt;
}
body.tblt-m .top_text_2 {
right: 8mm;
}
.vertical {
writing-mode: vertical-rl;
/*text-orientation: upright;*/
@@ -367,6 +385,10 @@ pre {
left: 2mm;
}
.top_text_3 {
display: none;
}
/*舊式紙張*/
body.tablet-l {
--page-w: 274mm;
@@ -548,6 +570,9 @@ body.tblt-l.a3.l2b .mid_text {
--divh: 60mm;
top: 40%;
}
body.tblt-l.a3 .left_text {
--font-max: 36pt;
}
body.tblt-l.a3.l2b .mid_text_2 {
--divh: 180mm;
@@ -638,7 +663,8 @@ body.tblt-m.a4-mrg {
}
body.tblt-m.a4-mrg .page {
--page-w: 100mm;
--page-w: 95mm;
margin-left:5mm;
float: left;
}
@@ -763,9 +789,8 @@ body.tblt-xs {
}
body.tblt-xs.a3-mrg .page {
/*--page-w: 57mm;*/
--page-w: 59mm;
--page-h: 100mm;
--page-w: 57mm;
--page-h: 98mm;
page-break-after: auto;
float: left;
}
@@ -775,13 +800,11 @@ body.tblt-xs.a4l-mrg {
}
body.tblt-xs.a4l-mrg .page {
/* --page-w: 57mm; */
--page-w: 59mm;
--page-h: 100mm;
--page-w: 57mm;
--page-h: 98mm;
page-break-after: auto;
float: left;
}
body.tblt-xs .top_text_1 {
font-size: 8pt;
}
@@ -800,9 +823,9 @@ body.tblt-xs .mid_text_2 {
}
body.tblt-xs .right_text {
--divw: 12mm;
--divw: 10mm;
--divh: 60mm;
right: 0mm;
right: 2mm;
top: 25mm;
}
@@ -825,8 +848,7 @@ body.tblt-xs .top_text_2{
/*[標準X2B]隨喜牌位-佛力超薦-往生蓮位-A3(直)*/
right: 2mm;
right: 3.0mm;
right: 3.5mm;
/* right: 4.0mm; */
right: 5mm;
}
body.tblt-xs .txt_up {
@@ -1075,35 +1097,4 @@ body.prayer-h pre {
/*[標準X1A]隨喜牌位-佛光注照-長生祿位-A4(橫) */
* {
outline:0 none !important;
}
/*让自定义的分页生效在某个div后面产生class-page从而分页当牌位不是同一家人的时候就分页*/
@media print {
.page-break {
display: block !important;
width: 100%;
height: 1px;
visibility: hidden;
page-break-after: always;
}
.page {
page-break-inside: auto !important;
}
@page {
orphans: 1;
widows: 1;
}
.d-flex {
display: block !important;
}
.page-break::after {
content: "";
display: block;
height: 1px;
visibility: hidden;
}
}
}

View File

@@ -32,17 +32,25 @@ function init_print() {
//console.log(i, $(this).html(),$(this).text());
//console.log(i, txt==htm);
//console.log(i, txt_arr, line, line_len);
line_len = Math.ceil(line_len / 5) * 5;
line_len = Math.ceil(line_len / 3) * 3;
//if(line>1){
css = $(this).attr("style")||"";
if(css.length>0){
css += ";";
}
css += "--lines:" + line + ";--line_len:" + line_len + ";";
//debugger;
//字少時, 加空間
let allShort = txt_arr2.every(line => line.trim().length <= 7);
let addSpaceClass = allShort ? "add-space" : "";
allShort = txt_arr2.every(line => line.trim().length <= 3);
addSpaceClass += allShort ? " add-space-3" : "";
htm_lines = txt_arr2.join("<br>");
$(this).attr("style", css);
$(this).html(htm_lines);
$(this)
.attr("style", css)
.addClass(addSpaceClass)
.html(htm_lines);
//}
//console.log(i, line, line_len, css,txt);
});

View File

@@ -153,6 +153,9 @@
<template #item.slot_type ="{item}">
{{item.detail.actItem?.substring(0,3)}}
</template>
<template #item.up_time ="{item}">
{{item.up_time|timeString('YYYY-MM-DD')}}
</template>
<template #item.print="{item}">
<v-icon color="blue" class="mr-2" @click="yulan_single(item);" >
mdi-printer

View File

@@ -16,7 +16,7 @@
</div>
<div class="card-body">
<asp:Literal ID="L_msg" runat="server" />
<asp:HiddenField ID="HF_Id" runat="server" />
<asp:HiddenField ID="HF_Code" runat="server" />
<div class="form-group mb-3">
<label for="TB_Name">名稱</label>

View File

@@ -1,35 +1,36 @@
using Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class admin_region_bed_bedstatus_create : MyWeb.config
{
private Model.ezEntities _db = new Model.ezEntities();
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack) // 加這行
if (!IsPostBack)
{
if (Guid.TryParse(Request.QueryString["statusid"], out Guid id))
var code = Request.QueryString["code"];
if (!string.IsNullOrEmpty(code))
{
LoadData(id);
LoadData(code);
L_title.Text = "編輯區域類型";
}
var categoryList = RegionRoomBedStatus.GetCategoryList();
// 假设你的下拉控件ID叫 ddlCategory
TB_Category.DataSource = categoryList;
TB_Category.DataTextField = "Text"; // 示文字
TB_Category.DataValueField = "Value"; // 选项
TB_Category.DataTextField = "Text"; // 示文字
TB_Category.DataValueField = "Value"; // 選項
TB_Category.DataBind();
TB_Category.Items.Insert(0, new ListItem("--请选择分类--", ""));
TB_Category.Items.Insert(0, new ListItem("--請選擇分類--", ""));
}
}
private void LoadData(Guid id)
private void LoadData(string code)
{
var rt = _db.RegionRoomBedStatus.FirstOrDefault(r => r.Uuid == id);
var rt = _db.RegionRoomBedStatus.FirstOrDefault(r => r.Code == code);
if (rt == null)
{
L_msg.Text = "<div class='alert alert-danger'>找不到資料</div>";
@@ -37,22 +38,24 @@ public partial class admin_region_bed_bedstatus_create : MyWeb.config
return;
}
HF_Id.Value = rt.Uuid.ToString();
HF_Code.Value = rt.Code; // ✅ 以 Code 為唯一識別
TB_Name.Text = rt.Name;
TB_Code.Text = rt.Code;
TB_Category.SelectedValue = rt.Category?.ToString();
Description.Text = rt.Description;
}
protected void BTN_Save_Click(object sender, EventArgs e)
{
try
{
RegionRoomBedStatus rt;
if (Guid.TryParse(HF_Id.Value, out Guid id))
var code = HF_Code.Value; // ✅ 用隱藏欄位的 Code 判斷
if (!string.IsNullOrEmpty(code))
{
// 更新
rt = _db.RegionRoomBedStatus.FirstOrDefault(r => r.Uuid == id);
rt = _db.RegionRoomBedStatus.FirstOrDefault(r => r.Code == code);
if (rt == null)
{
L_msg.Text = "<div class='alert alert-danger'>資料不存在</div>";
@@ -63,19 +66,20 @@ public partial class admin_region_bed_bedstatus_create : MyWeb.config
{
// 新增
rt = new RegionRoomBedStatus();
rt.Uuid = Guid.NewGuid();
rt.Code = TB_Code.Text.Trim(); // ✅ 以 Code 當主鍵
_db.RegionRoomBedStatus.Add(rt);
}
rt.Name = TB_Name.Text.Trim();
if (rt.Name.Length == 0)
if (string.IsNullOrEmpty(rt.Name))
{
L_msg.Text = "<div class='alert alert-danger'>名稱不能空</div>";
L_msg.Text = "<div class='alert alert-danger'>名稱不能空</div>";
return;
}
rt.Code = TB_Code.Text.Trim();
rt.Description = Description.Text.Trim();
if(int.TryParse(TB_Category.SelectedValue, out int category))
if (int.TryParse(TB_Category.SelectedValue, out int category))
{
rt.Category = category;
}
@@ -83,16 +87,18 @@ public partial class admin_region_bed_bedstatus_create : MyWeb.config
{
rt.Category = null;
}
_db.SaveChanges();
L_msg.Text = "<div class='alert alert-success'>儲存成功</div>";
// 如果是新增,更新隱藏欄位並切換標題為編輯
if (HF_Id.Value == "")
// 如果是新增,更新隱藏欄位並切換標題
if (string.IsNullOrEmpty(HF_Code.Value))
{
HF_Id.Value = rt.Uuid.ToString();
HF_Code.Value = rt.Code;
L_title.Text = "編輯區域類型";
}
Response.Redirect("index.aspx");
}
catch (Exception ex)
@@ -100,4 +106,4 @@ public partial class admin_region_bed_bedstatus_create : MyWeb.config
L_msg.Text = $"<div class='alert alert-danger'>錯誤:{ex.Message}</div>";
}
}
}
}

View File

@@ -20,7 +20,7 @@
:loading="loading"
>
<template #item.actions="{item}">
<a :href="'create.aspx?statusid='+item.uuid" class="btn btn-primary"><i class="mdi mdi-pencil"></i>修改</a>
<a :href="'create.aspx?code='+item.code" class="btn btn-primary"><i class="mdi mdi-pencil"></i>修改</a>
<button
type="button"
class="btn btn-outline-danger"
@@ -46,11 +46,10 @@
data() {
return {
headers: [
{ text: 'Id', value: 'id' },
{ text: '狀態名稱', value: 'name' },
{ text: '狀態代碼', value: 'code' },
{ text: '描述', value: 'description' },
{ text: '状态分类', value: 'categoryName'},
{ text: '状态分类', value: 'categoryName' },
{ text: '', value: 'actions' }
],
items: [],
@@ -59,7 +58,7 @@
},
methods: {
getStatusList() {
axios.get('/api/region/bed/status/list')
axios.get(HTTP_HOST + 'api/region/bed/status/list')
.then((res) => {
this.items = res.data
this.loading = false;
@@ -74,11 +73,11 @@
})
},
deleteStatus(item) {
axios.post('/api/region/bed/status/delete', null, {
params: { id: item.uuid }
axios.post(HTTP_HOST + 'api/region/bed/status/delete', null, {
params: { code: item.code }
})
.then(() => {
this.items = this.items.filter(i => i.uuid != item.uuid);
this.items = this.items.filter(i => i.code != item.code);
this.$refs.messageModal.open({
title: '操作成功',
message: '刪除成功!',

View File

@@ -108,11 +108,12 @@
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<div v-for="region in regions" :key="region.uuid" class="region-block mb-4">
<h2 class="region-title mb-3">{{ region.regionPath }}</h2>
<div v-for="region in regions" :key="region.uuid" class="region-block mb-4"
:style="region.isStop ? { pointerEvents: 'none', opacity: '0.6', backgroundColor: '#f8d7da' } : {}">
<h2 class="region-title mb-3">{{region.isStop?(region.regionPath + '(已停用)'): region.regionPath }}</h2>
<div class="row g-3 justify-content-start">
<div v-for="room in region.room" :key="room.ruid" class="col-12 col-md-6 col-lg-4">
<div v-for="room in region.room" :key="room.ruid" class="col-12 col-md-6 col-lg-6">
<div class="card h-100 shadow-sm" style="min-height: 300px; max-height: 400px; overflow-y: auto; border-radius: 0.5rem;">
<!-- 上部:房間名稱 -->
@@ -138,20 +139,26 @@
<thead>
<tr>
<th scope="col">床位名稱</th>
<th scope="col">是否可用</th>
<th scope="col">使用明細</th>
<th scope="col">掛單</th>
<th scope="col">當前狀態</th>
</tr>
</thead>
<tbody>
<tr v-for="bed in room.beds" :key="bed.uuid"
:class="bed.canuse ? 'table-success' : 'table-danger'">
<td>{{ bed.name }}</td>
<td :class="!bed.canuse ? 'text-danger' : 'text-success'">
{{ bed.canuse ? '是' : '否' }}
</td>
<td>
<button type="button" class="btn btn-primary" @click="showBedSchedule(bed)">查看明細</button>
</td>
<td>
<button type="button" class="btn btn-primary">
快速掛單
</button>
</td>
<td>
{{bed.statusname}}
</td>
</tr>
</tbody>
</table>
@@ -165,43 +172,49 @@
</div>
</div>
</div>
<div>
<v-dialog v-model="bedSchedule.dialogVisible" max-width="900">
<v-card>
<v-card-title>
<span class="text-h6">床位排程明細 - {{ bedSchedule.selectedBed?.name }}</span>
<v-spacer></v-spacer>
<v-btn icon @click="closeBedSchedule">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<div>
<v-dialog v-model="bedSchedule.dialogVisible" max-width="900px">
<v-card
style="min-height:50vh; max-height:80vh; display:flex; flex-direction:column;"
>
<v-card-title>
<span class="text-h6">床位排程明細 - {{ bedSchedule.selectedBed?.name }}</span>
<v-spacer></v-spacer>
<v-btn icon @click="closeBedSchedule"><v-icon>mdi-close</v-icon></v-btn>
</v-card-title>
<v-card-text>
<v-data-table
:headers="bedSchedule.scheduleHeaders"
:items="bedSchedule.selectedBed?.schedules || []"
class="elevation-1"
dense
hide-default-footer
:items-per-page="5"
>
<template #item.scheduleDate="{item}">
{{item.scheduledate|timeString('YYYY-MM-DD')}}
</template>
<template #item.actions =" {item}">
<a :href="'/admin/guadan/create.aspx?orderId='+item.guaDanOrderNo" class="btn btn-primary">查看掛單</a>
</template>
</v-data-table>
</v-card-text>
<!-- 关键改动flex:1 1 auto; min-height:0; overflow-y:auto -->
<v-card-text style="flex:1 1 auto; min-height:0; overflow-y:auto;">
<div style="min-height:0;">
<v-data-table
:headers="bedSchedule.scheduleHeaders"
:items="bedSchedule.selectedBed?.schedules || []"
:items-per-page="9999"
class="elevation-1"
dense
hide-default-footer
>
<template #item.scheduleDate="{item}">
{{ item.scheduledate | timeString('YYYY-MM-DD') }}
</template>
<template #item.actions="{item}">
<a :href="'/admin/guadan/create.aspx?orderId='+item.guaDanOrderNo" class="btn btn-primary">
查看掛單
</a>
</template>
</v-data-table>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text color="primary" @click="closeBedSchedule">關閉</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text color="primary" @click="closeBedSchedule">關閉</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
@@ -251,6 +264,7 @@
{ text: '使用日期', value: 'scheduledate' },
{ text: '掛單單號', value: 'guaDanOrderNo' },
{ text: '標題', value: 'title' },
{ text: '掛單人', value: 'usename' },
{ text: '查看掛單', value: 'actions' },
],
},
@@ -302,10 +316,18 @@
unoccupied: this.filter.unoccupied,
gender: this.filter.Gender,
};
axios.post('/api/region/list', payload)
axios.post(HTTP_HOST + 'api/region/list', payload)
.then((res) => {
this.regions = res.data.regions;
this.summary = res.data.summary; // 保存後端統計
try {
this.regions.sort((a, b) => {
return Number(a.isStop) - Number(b.isStop);
});
}
catch (error) {
console.log(error)
}
})
.catch((err) => {
console.error('API 錯誤', err);
@@ -325,7 +347,7 @@
console.log(this.filter.Gender);
},
getRegionWithRoom() {
axios.get('/api/region/regionwithroom')
axios.get(HTTP_HOST + 'api/region/regionwithroom')
.then((res) => {
this.filter.areas = res.data;
})
@@ -381,9 +403,6 @@
return { maleBeds, femaleBeds };
}
}
})
</script>
</asp:Content>

View File

@@ -0,0 +1,217 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="handle_bed_in_used.aspx.cs" Inherits="admin_region_handle_bed_in_used" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" Runat="Server">
<nav>
<div>
<label style="display: inline-block;">區域:</label>
<select style="display: inline-block; min-width:150px; max-width:20%;"
class="form-select"
v-model="selectedRegionUuid"
:disabled="isFromUrl"
@change="onRegionChange"
><option :value="null">請選擇區域</option>
<option
v-for="region in regions"
:key="region.uuid"
:value="region.uuid"
>
{{ region.name }}
</option>
</select>
<label style="display: inline-block;margin-left: 20px;">客房:</label>
<select style="display: inline-block; min-width:200px; max-width:20%;"
class="form-select"
v-model="selectedRoomUuid"
:disabled="isFromUrl"
@change="onRoomChange"
>
<option :value="null">請選擇房間</option>
<option v-for="room in rooms" :key="room.uuid" :value="room.uuid">{{room.fullName}}</option>
</select>
<label style="display: inline-block;margin-left: 20px;">床位:</label>
<select style="display: inline-block; width:100px;"
class="form-select"
v-model="selectedBedUuid"
:disabled="isFromUrl"
></select>
</div>
</nav>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<div>
<v-data-table
:items="items"
show-select
v-model:selected="selectedItems"
item-key="bedUuid"
:headers="headers">
<template #item.actions="{item}">
<button class="btn btn-outline-danger" type="button" @click="confirmAndCancelSingleBedBooking(item)">取消預約</button>
</template>
<template #item.guadan_during="{item}">
{{item.guadan_during.checkInAt|timeString('YYYY-MM-DD')}} - {{item.guadan_during.checkOutAt|timeString('YYYY-MM-DD')}}
</template>
<template #item.status="{item}">
{{item.status.name}}
</template>
</v-data-table>
</div>
<!-- 更新修改確認彈出視窗 -->
<message-modal ref="messageModal"></message-modal>
<!-- 刪除確認彈出視窗 -->
<confirm-modal ref="confirmModal"></confirm-modal>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" Runat="Server">
<script>
Vue.filter('timeString', function (value, myFormat) {
return value == null || value == "" ? "" : moment(value).format(myFormat || 'YYYY-MM-DD, HH:mm:ss');
});
new Vue({
el: '#app',
vuetify: new Vuetify(vuetify_options),
data() {
return {
regionUuidFromUrl: '<%= Request.QueryString["region"] %>' || null,
roomUuidFromUrl: '<%= Request.QueryString["room"] %>' || null,
bedUuidFromUrl: '<%= Request.QueryString["bed"] %>' || null,
// 用戶選擇的值
selectedRegionUuid: null,
selectedRoomUuid: null,
selectedBedUuid: null,
regions: [],
rooms: [],
bed: [],
selectedItems: [],
items: [],
headers: [
{ text: '床位名稱', value: 'fullName' },
{ text: '掛單號:', value: 'guaDanOrderNo' },
{ text: '掛單人', value: 'u_name' },
{ text: '掛單時間', value: 'guadan_during' },
{ text: '掛單狀態', value: 'status' },
{ text: '', value: 'actions' },
]
}
},
methods: {
onRegionChange() {
console.log("選擇的區域 UUID:", this.selectedRegionUuid);
this.selectedRoomUuid = null;
this.selectedBedUuid = null;
this.GetInUsedBed();
this.GetRoomList();
},
onRoomChange() {
console.log("選擇的客房 UUID:", this.selectedRoomUuid);
this.GetInUsedBed();
},
async GetInUsedBed() {
//獲取已經預約或者入住的床位
try {
const payload = {
regionUuid: this.selectedRegionUuid || null,
roomUuid: this.selectedRoomUuid || null,
bedUuid: this.selectedBedUuid || null
};
const response = await axios.post(HTTP_HOST + "api/bed/inuse/list", payload);
// 假設返回的就是床位數組
this.items = response.data;
console.log("已獲取床位:");
} catch (error) {
console.error("獲取床位失敗:", error);
}
},
async confirmAndCancelSingleBedBooking(item) {
// 先彈出確認彈出視窗
this.$refs.confirmModal.open({
message: `確定要取消床位 ${item.name || ''} 的所有預約嗎?`,
onConfirm: async () => {
try {
const payload = {
bedUuid: item.bedUuid || null
};
const response = await axios.post(
HTTP_HOST + "api/bed/inuse/cancel/singlebed/booking",
payload
);
// 刷新床位列表
this.GetInUsedBed();
// 成功提示
console.log("取消成功:", item.bedUuid);
this.$refs.messageModal.open({
title: '取消成功',
message: response?.data?.message || '取消成功!',
status: 'success'
});
} catch (error) {
console.error("取消失敗:", error);
this.$refs.messageModal.open({
title: '取消失敗',
message: error.response?.data?.message || '取消過程中發生錯誤!',
status: 'error'
});
}
}
});
},
async CancelAllBedBooking() {
//取消符合條件的所有床位的所有預約
},
async GetRegionList() {
try {
const response = await axios.get(
HTTP_HOST + "api/bed/inuse/region/list"
);
this.regions = response.data;
} catch (error) {
}
},
async GetRoomList() {
try {
const response = await axios.get(
HTTP_HOST + "api/bed/inuse/room/list", {
params: {
regionUuid: this.selectedRegionUuid
}
}
);
this.rooms = response.data;
} catch (error) {
}
},
},
watch: {
},
mounted() {
this.selectedRegionUuid = this.regionUuidFromUrl;
this.selectedRoomUuid = this.roomUuidFromUrl;
this.selectedBedUuid = this.bedUuidFromUrl;
this.GetInUsedBed();
this.GetRegionList();
this.GetRoomList();
},
computed: {
// 判斷是否來自 URL禁用下拉框
isFromUrl() {
return this.regionUuidFromUrl || this.roomUuidFromUrl || this.bedUuidFromUrl;
}
}
});
</script>
</asp:Content>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class admin_region_handle_bed_in_used : MyWeb.config
{
protected void Page_Load(object sender, EventArgs e)
{
}
}

View File

@@ -15,17 +15,16 @@
<i class="mdi mdi-arrow-collapse-all"></i> 全部收起
</button>
</nav>
<nav v-if="form && selectedType==null">
<nav v-if="createRegionFlag">
<button class="btn btn-primary me-2" @click="saveRegion" type="button">
<i class="bi bi-save me-1"></i> 儲存區域資料
</button>
</nav>
<nav class="btn-group mb-2 ps-3 pe-3" role="group" v-if="form && selectedType=='region'">
<button class="btn btn-primary me-2" @click="saveRegion" type="button">
<i class="bi bi-save me-1"></i> 儲存區域資料
</button>
<nav class="btn-group mb-2 ps-3 pe-3" role="group" v-if="form && selectedType=='region' && !createRegionFlag">
<div v-if="selectedRegionId">
<button class="btn btn-primary me-2" @click="saveRegion" type="button">
<i class="bi bi-save me-1"></i> 儲存區域資料1
</button>
<button class="btn btn-success me-2" @click="createSubRegion" type="button" >
<i class="mdi mdi-arrow-down-right"></i> 新增下層區域
</button>
@@ -217,8 +216,8 @@
<template #item.isactive="{item}">
{{item.isactive ? '啟用' : '停用'}}
</template>
<template #item.statusuuid="{item}">
{{getBedStatusNameById(item.statusuuid)}}
<template #item.statuscode="{item}">
{{getBedStatusNameById(item.statuscode)}}
</template>
<template #item.action ="{item}">
<button type="button" class="btn btn-primary" @click="editBed(item)">
@@ -252,8 +251,8 @@
<div class="mb-3">
<label class="form-label">狀態</label>
<select class="form-control" v-model="room_bed.newBedForm.statusuuid">
<option v-for="status in room_bed.bed_status" :value="status.uuid">
<select class="form-control" v-model="room_bed.newBedForm.statuscode" disabled>
<option v-for="status in room_bed.bed_status" :value="status.code">
{{status.name}}
</option>
</select>
@@ -349,6 +348,23 @@
</v-card-actions>
</v-card>
</v-dialog>
<!-- 停用區域如果有床位正在掛單提示彈出視窗 -->
<div v-if="bed_is_used_modal" style="position:fixed; top:0; left:0; width:100%; height:100%;
background:rgba(0,0,0,0.5); display:flex; align-items:center; justify-content:center; z-index:9999;">
<div style="background:#fff; padding:20px; border-radius:8px; width:300px; text-align:center;">
<p style="margin-bottom:15px;">{{bed_is_used_modal_message}}</p>
<div style="display:flex; justify-content:flex-end; gap:10px;">
<button @click="closeBedInUsedModal"
class="btn btn-danger"
type="button" style="padding:6px 12px; border:none; border-radius:4px; cursor:pointer;">关闭</button>
<a :href="bed_is_used_modal_link" target="_blank" class="btn btn-primary">
前往处理
</a>
</div>
</div>
</div>
<!-- 更新修改確認彈出視窗 -->
<message-modal ref="messageModal"></message-modal>
<!-- 刪除確認彈出視窗 -->
@@ -477,13 +493,14 @@
</div>
`
});
new Vue({
el: '#app',
vuetify: new Vuetify(vuetify_options),
data() {
return {
bed_is_used_modal: false,
bed_is_used_modal_message: null,
bed_is_used_modal_link: "handle_bed_in_used.aspx", // 默认链接
selectedId: null, // 被選中項目ID
selectedType: null, // 'region' 或 'room'
expandAllFlag: false, // 控制全部展開
@@ -499,6 +516,7 @@
regionTypes: [],
currentSelectRegion: null,
currentSelectRoom: null,
createRegionFlag: false,
form: {
uuid: null,
name: '',
@@ -528,7 +546,7 @@
bed_headers: [
{ text: '床位編號', value: 'uuid' },
{ text: '床位名稱', value: 'name' },
{ text: '床位狀態', value: 'statusuuid' },
{ text: '床位狀態', value: 'statuscode' },
{ text: '是否啟用', value: 'isactive' },
{ text: '', value: 'action' },
],
@@ -536,7 +554,7 @@
uuid: null,
RegionUuid: null,
Name: '',
statusuuid: null,
statuscode: null,
IsActive: true,
Gender: null,
},
@@ -546,6 +564,11 @@
};
},
methods: {
closeBedInUsedModal() {
this.bed_is_used_modal = false;
this.bed_is_used_modal_message = null;
this.bed_is_used_modal_link = "handle_bed_in_used.aspx"; // 默认链接
},
expandAll() {
this.expandAllFlag = true;
this.collapseAllFlag = false;
@@ -555,7 +578,7 @@
this.expandAllFlag = false;
},
async loadRegions() {
const res = await axios.post('/api/region/getRegionList');
const res = await axios.post(HTTP_HOST + 'api/region/getRegionList');
this.regions = res.data;
this.flatRegions = this.flatten(res.data);
if (this.currentSelectRoom) {
@@ -563,7 +586,7 @@
}
},
loadRegionType() {
axios.post('/api/region/getRegionType')
axios.post(HTTP_HOST + 'api/region/getRegionType')
.then(res => {
this.regionTypes = res.data
});
@@ -582,6 +605,7 @@
this.selectedType = 'region';
this.selectedRegionId = region.uuid;
this.currentSelectRegion = region;
console.log(this.currentSelectRegion)
this.currentSelectRoom = null;
this.resetRoomForm();
this.form = {
@@ -612,6 +636,8 @@
this.disabledParentOptions = [];
this.currentSelectRegion = null;
this.currentSelectRoom = null;
this.createRegionFlag = true;
this.selectedType = null;
},
createSubRegion() {
if (!this.selectedRegionId) return;
@@ -633,25 +659,39 @@
});
return;
}
const url = this.form.uuid ? '/api/region/update' : '/api/region/create';
const url = this.form.uuid
? HTTP_HOST + 'api/region/update'
: HTTP_HOST + 'api/region/create';
axios.post(url, this.form)
.then((res) => {
//alert('儲存成功');
this.loadRegions();
//this.newRegion();
this.form.uuid = res.data.uuid;
this.selectedRegionId = res.data.uuid;
this.form.uuid = res.data.id;
this.selectedRegionId = res.data.id;
this.currentSelectRegion = JSON.parse(JSON.stringify(this.form));
this.createRegionFlag = false;
this.$refs.messageModal.open({
title: "更新",
message: "更新成功",
});
})
.catch((error) => {
this.$refs.messageModal.open({
title: '更新提示',
message: error.response?.data?.message || "儲存失敗,請稍後再試。",
});
console.error('更新失敗', error);
const code = error.response?.data?.code;
const message = error.response?.data?.message || error.message ||
"未知錯誤,請稍後再試";
if (code === "BED_IS_USED") {
this.bed_is_used_modal = true;
this.bed_is_used_modal_message = message
this.bed_is_used_modal_link = this.bed_is_used_modal_link + '?region=' + this.form.uuid
} else {
this.$refs.messageModal.open({
message: (message)
});
}
});
},
deleteRegion() {
@@ -664,7 +704,7 @@
});
},
confirmDeleteRegion() {
axios.post('/api/region/delete', { uuid: this.form.uuid })
axios.post(HTTP_HOST + 'api/region/delete', { Uuid: this.form.uuid })
.then(() => {
this.showDeleteModal = false;
this.$refs.messageModal.open({
@@ -716,7 +756,7 @@
uuid: null,
RoomUuid: this.currentSelectRoom.uuid,
Name: '',
statusuuid: null,
statuscode: "101",
IsActive: true,
Gender: this.currentSelectRoom.gender, // 不設預設值,強制選擇
};
@@ -731,7 +771,7 @@
return;
}
try {
var res = await axios.post('/api/region/bed/create', this.room_bed.newBedForm);
var res = await axios.post(HTTP_HOST + 'api/region/bed/create', this.room_bed.newBedForm);
this.room_bed.showBedModal = false;
this.$refs.messageModal.open({
title: '成功',
@@ -759,7 +799,7 @@
});
},
confirmDeleteBed(bed) {
axios.post('/api/region/bed/delete', null, {
axios.post(HTTP_HOST + 'api/region/bed/delete', null, {
params: { uuid: bed.uuid }
}) // 假設後端吃的是 id
.then(() => {
@@ -791,7 +831,7 @@
RegionUuid: bed.regionUuid,
RoomUuid: bed.roomUuid,
Name: bed.name,
statusuuid: bed.statusuuid,
statuscode: bed.statuscode,
IsActive: bed.isactive,
Gender: bed.gender,
};
@@ -800,7 +840,7 @@
async saveEditBed() {
try {
await axios.post('/api/region/bed/update', this.room_bed.newBedForm);
await axios.post(HTTP_HOST + 'api/region/bed/update', this.room_bed.newBedForm);
this.room_bed.showBedModal = false;
const updated = this.room_bed.newBedForm;
@@ -811,7 +851,7 @@
...this.room_bed.bed_items[index], // 保留原本未更新欄位
roomUuid: updated.RoomUuid,
name: updated.Name,
statusuuid: updated.statusuuid,
statuscode: updated.statuscode,
isactive: updated.IsActive
});
}
@@ -824,25 +864,31 @@
await this.loadRegions();
this.room_bed.bed_items = this.currentSelectRoom.beds;
//this.selectRegion(this.findRegionById(this.regions, this.form.id));
} catch (err) {
console.log(err)
this.$refs.messageModal.open({
title: '錯誤',
message: err.response?.data?.message || '更新失敗'
});
} catch (error) {
console.error('更新失敗', error);
const code = error.response?.data?.code;
const message = error.response?.data?.message || error.message ||
"未知錯誤,請稍後再試";
if (code === "BED_IS_USED") {
this.bed_is_used_modal = true;
this.bed_is_used_modal_message = message;
this.bed_is_used_modal_link = this.bed_is_used_modal_link + '?bed=' + this.room_bed.newBedForm.uuid
} else {
this.$refs.messageModal.open({
message: (message)
});
}
}
},
getBedStatus() {
//獲取床位狀態
axios.get('/api/region/bed/status/list')
axios.get(HTTP_HOST + 'api/region/bed/status/list')
.then((res) => {
this.room_bed.bed_status = res.data;
})
},
getBedStatusNameById(id) {
console.log(id)
//傳入一個Id獲取該Id對應的名稱
const status = this.room_bed.bed_status.find(i => i.uuid == id);
getBedStatusNameById(statuscode) {
const status = this.room_bed.bed_status.find(i => i.code == statuscode);
if (status) {
return status.name;
}
@@ -861,7 +907,7 @@
});
return;
}
axios.post('/api/region/room/create', this.room.room_form)
axios.post(HTTP_HOST + 'api/region/room/create', this.room.room_form)
.then((res) => {
this.room.showCreateRoomDialog = false;
this.currentSelectRegion.rooms.push(res.data);
@@ -878,7 +924,7 @@
},
async roomUpdate() {
try {
const res = await axios.post('/api/region/room/update', this.room.room_form);
const res = await axios.post(HTTP_HOST + 'api/region/room/update', this.room.room_form);
this.$refs.messageModal.open({
message: '客房資料更新成功'
});
@@ -888,14 +934,23 @@
};
} catch (error) {
console.error('更新失敗', error);
this.$refs.messageModal.open({
message: (error.response?.data?.message || error.message)
});
const code = error.response?.data?.code;
const message = error.response?.data?.message || error.message ||
"未知錯誤,請稍後再試";
if (code === "BED_IS_USED") {
this.bed_is_used_modal = true;
this.bed_is_used_modal_message = message;
this.bed_is_used_modal_link = this.bed_is_used_modal_link + '?room=' + this.room.room_form.uuid
} else {
this.$refs.messageModal.open({
message: (message)
});
}
}
},
roomDelete() {
axios.post('/api/region/room/delete', { uuid: this.currentSelectRoom.uuid })
axios.post(HTTP_HOST + 'api/region/room/delete', { uuid: this.currentSelectRoom.uuid })
.then((res) => {
const region = this.findRegionById(this.regions, this.currentSelectRoom.regionUuid)//當前room所在的region
if (region) {
@@ -908,6 +963,10 @@
this.currentSelectRoom = null;
this.room_bed.bed_items = [];
//清空 beds
}).catch((error) => {
this.$refs.messageModal.open({
message: (error.response?.data?.message || error.message)
});
});
},
confirmRoomDelete() {
@@ -944,7 +1003,16 @@
},
watch: {
currentSelectRegion(newVal) {
if (newVal !== null) {
this.createRegionFlag = false;
}
},
currentSelectRoom(newVal) {
if (newVal !== null) {
this.createRegionFlag = false;
}
}
},
mounted() {
this.loadRegions();

View File

@@ -61,7 +61,7 @@
},
methods: {
getRegionTypeList() {
axios.post('/api/regiontype/getreiontypelist')
axios.post(HTTP_HOST + 'api/regiontype/getreiontypelist')
.then((res) => {
this.items = res.data;
})
@@ -71,7 +71,7 @@
'title': '刪除提示',
'message': `確定要刪除 ${item.name} ?`,
onConfirm: () => {
axios.post('/api/regiontype/delete',null, {
axios.post(HTTP_HOST + 'api/regiontype/delete',null, {
params: { uuid: item.uuid }
})
.then(() => {

View File

@@ -18,13 +18,13 @@
<div class="">
<a class="btn btn-outline-primary btn-print" @click="updateShuWen" v-if="currentActivityNum&&!latest">{{updateShuWenLoading ? '更新中': '更新疏文'}}</a>
<a :href="'/api/shuwen/download?activitynum='+currentActivityNum" class="btn btn-outline-primary btn-print">下載疏文Word檔</a>
<a :href="'<%=ResolveUrl("~/api/shuwen/download")%>?activitynum='+currentActivityNum" class="btn btn-outline-primary btn-print">下載疏文Word檔</a>
</div>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<div v-if="currentActivityNum">
<div><h2 style="text-align:center">消災疏文</h2></div>
<div v-for="item in ShuWenJson.xiaozai" :key="Object.keys(item)[0]">
<div v-for="(item, index) in ShuWenJson?.xiaozai" :key="Object.keys(item)[0] + 'xz' + index">
<h4>報名信眾:{{ item[Object.keys(item)[0]].user.name }}</h4>
<ul>
<li>{{ item[Object.keys(item)[0]]['biaoti'].join(' ') }}</li>
@@ -32,7 +32,7 @@
<hr />
</div>
<div><h2 style="text-align:center">超薦疏文</h2></div>
<div v-for="item in ShuWenJson.chaodu" :key="Object.keys(item)[0]">
<div v-for="(item, index) in ShuWenJson?.chaodu" :key="Object.keys(item)[0] + 'cj' + index">
<h4>報名信眾:{{ item[Object.keys(item)[0]].user.name }}</h4>
<ul>
<li>
@@ -113,7 +113,10 @@
this.ShuWenJson = {}
return;
}
this.ShuWenJson = JSON.parse(this.ShuWenItem.shuWenList)
this.ShuWenJson = this.ShuWenItem?.shuWenList
? JSON.parse(this.ShuWenItem.shuWenList)
: { xiaozai: [], chaodu: [] }; // 默认空对象
})
.catch(err => {
if (err.response && err.response.status === 400) {