1142 lines
48 KiB
C#
1142 lines
48 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Web.Http;
|
|
using PagedList;
|
|
using Newtonsoft.Json;
|
|
using System.Collections;
|
|
using System.Data.Entity;
|
|
|
|
// api/pivot
|
|
[ezAuthorize]
|
|
public class pivotController : ApiController
|
|
{
|
|
private Model.ezEntities _db = new Model.ezEntities();
|
|
|
|
#region 報表查詢 API
|
|
|
|
/// <summary>
|
|
/// 報名明細查詢 - 對應 Excel 中的視圖結構
|
|
/// GET api/pivot/registration_details
|
|
/// </summary>
|
|
/// <param name="startDate">開始日期</param>
|
|
/// <param name="endDate">結束日期</param>
|
|
/// <param name="activityNum">法會編號</param>
|
|
/// <param name="followerNum">信眾編號</param>
|
|
/// <param name="page">頁碼</param>
|
|
/// <param name="pageSize">每頁筆數</param>
|
|
/// <returns></returns>
|
|
[HttpGet]
|
|
[Route("api/pivot/registration_details")]
|
|
public IHttpActionResult GetRegistrationDetails(string startDate = null, string endDate = null,
|
|
int? activityNum = null, int? followerNum = null, int page = 1, int pageSize = 1000)
|
|
{
|
|
try
|
|
{
|
|
// 對應 README.md 中的視圖查詢結構,按顏色分類欄位
|
|
var query = from pod in _db.pro_order_detail
|
|
join po in _db.pro_order on pod.order_no equals po.order_no
|
|
join ai in _db.actItems on pod.actItem_num equals ai.num
|
|
join act in _db.activities on po.activity_num equals act.num
|
|
join f in _db.followers on po.f_num equals f.num
|
|
join aik in _db.actItem_kind on ai.kind equals aik.num
|
|
select new
|
|
{
|
|
// 🟠 橘色欄位 - 法會基礎資料 (法會相關的直接來源)
|
|
法會ID = act.num,
|
|
法會名稱 = act.subject,
|
|
開始日期 = act.startDate_solar,
|
|
結束日期 = act.endDate_solar,
|
|
|
|
// 🔵 藍色欄位 - 信眾基礎資料 (信眾相關的直接來源)
|
|
信眾編號 = f.f_number,
|
|
信眾姓名 = f.u_name,
|
|
報名編號 = po.order_no,
|
|
報名日期 = po.up_time,
|
|
|
|
// 🟢綠色欄位 - 功德資訊欄位 (功德相關資訊)
|
|
功德主 = pod.parent_num != null ? "是" : "否",
|
|
功德類型 = aik.kind,
|
|
功德名稱 = ai.subject,
|
|
|
|
// 🟣 紫色欄位 - 計算欄位 (需要計算)
|
|
數量 = pod.qty,
|
|
金額 = pod.price,
|
|
已收 = 0, // 業務邏輯:固定為 0
|
|
未收 = pod.price * pod.qty - 0, // 計算未收金額
|
|
|
|
// 額外提供原始資料供進階計算
|
|
_metadata = new
|
|
{
|
|
parent_num = pod.parent_num,
|
|
unit_price = pod.price,
|
|
quantity = pod.qty,
|
|
total_amount = pod.price * pod.qty,
|
|
kind_num = aik.num,
|
|
item_num = ai.num,
|
|
activity_num = act.num,
|
|
follower_num = f.num
|
|
}
|
|
};
|
|
|
|
// 日期篩選
|
|
if (!string.IsNullOrEmpty(startDate) && DateTime.TryParse(startDate, out DateTime start))
|
|
{
|
|
query = query.Where(x => x.開始日期 >= start);
|
|
}
|
|
if (!string.IsNullOrEmpty(endDate) && DateTime.TryParse(endDate, out DateTime end))
|
|
{
|
|
query = query.Where(x => x.結束日期 <= end);
|
|
}
|
|
|
|
// 法會篩選
|
|
if (activityNum.HasValue)
|
|
{
|
|
query = query.Where(x => x.法會ID == activityNum.Value);
|
|
}
|
|
|
|
// 信眾篩選
|
|
if (followerNum.HasValue)
|
|
{
|
|
query = query.Where(x => x.信眾編號 == followerNum.ToString());
|
|
}
|
|
|
|
// 分頁處理
|
|
var totalCount = query.Count();
|
|
var pagedQuery = query.OrderByDescending(x => x.報名日期)
|
|
.ThenByDescending(x => x.法會ID)
|
|
.Skip((page - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.ToList();
|
|
|
|
var result = new
|
|
{
|
|
success = true,
|
|
data = new
|
|
{
|
|
list = pagedQuery,
|
|
total = totalCount,
|
|
page = page,
|
|
pageSize = pageSize,
|
|
totalPages = (int)Math.Ceiling((double)totalCount / pageSize)
|
|
},
|
|
message = "查詢成功"
|
|
};
|
|
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest($"查詢失敗:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 報名明細查詢 - Excel 匯出格式
|
|
/// GET api/pivot/registration_details_export
|
|
/// </summary>
|
|
/// <param name="startDate">開始日期</param>
|
|
/// <param name="endDate">結束日期</param>
|
|
/// <param name="activityNum">法會編號</param>
|
|
/// <param name="followerNum">信眾編號</param>
|
|
/// <returns></returns>
|
|
[HttpGet]
|
|
[Route("api/pivot/registration_details_export")]
|
|
public IHttpActionResult GetRegistrationDetailsForExport(string startDate = null, string endDate = null,
|
|
int? activityNum = null, int? followerNum = null)
|
|
{
|
|
try
|
|
{
|
|
// 完全對應 Excel 中的欄位結構
|
|
var query = from pod in _db.pro_order_detail
|
|
join po in _db.pro_order on pod.order_no equals po.order_no
|
|
join ai in _db.actItems on pod.actItem_num equals ai.num
|
|
join act in _db.activities on po.activity_num equals act.num
|
|
join f in _db.followers on po.f_num equals f.num
|
|
join aik in _db.actItem_kind on ai.kind equals aik.num
|
|
select new
|
|
{
|
|
// 完全對應 Excel 欄位名稱
|
|
法會ID = act.num,
|
|
法會名稱 = act.subject,
|
|
開始日期 = act.startDate_solar,
|
|
結束日期 = act.endDate_solar,
|
|
信眾編號 = f.f_number,
|
|
信眾姓名 = f.u_name,
|
|
報名編號 = po.order_no,
|
|
報名日期 = po.up_time,
|
|
功德主 = pod.parent_num != null ? "是" : "否",
|
|
功德類型 = aik.kind,
|
|
功德名稱 = ai.subject,
|
|
數量 = pod.qty,
|
|
金額 = pod.price,
|
|
已收 = 0,
|
|
未收 = pod.price * pod.qty
|
|
};
|
|
|
|
// 套用篩選條件
|
|
if (!string.IsNullOrEmpty(startDate) && DateTime.TryParse(startDate, out DateTime start))
|
|
{
|
|
query = query.Where(x => x.開始日期 >= start);
|
|
}
|
|
if (!string.IsNullOrEmpty(endDate) && DateTime.TryParse(endDate, out DateTime end))
|
|
{
|
|
query = query.Where(x => x.結束日期 <= end);
|
|
}
|
|
if (activityNum.HasValue)
|
|
{
|
|
query = query.Where(x => x.法會ID == activityNum.Value);
|
|
}
|
|
if (followerNum.HasValue)
|
|
{
|
|
query = query.Where(x => x.信眾編號 == followerNum.ToString());
|
|
}
|
|
|
|
var exportData = query.OrderByDescending(x => x.報名日期)
|
|
.ThenByDescending(x => x.法會ID)
|
|
.ToList()
|
|
.Select(x => new
|
|
{
|
|
x.法會ID,
|
|
x.法會名稱,
|
|
開始日期 = x.開始日期.HasValue ? x.開始日期.Value.ToString("yyyy/MM/dd") : "",
|
|
結束日期 = x.結束日期.HasValue ? x.結束日期.Value.ToString("yyyy/MM/dd") : "",
|
|
x.信眾編號,
|
|
x.信眾姓名,
|
|
x.報名編號,
|
|
報名日期 = x.報名日期.HasValue ? x.報名日期.Value.ToString("yyyy/MM/dd HH:mm") : "",
|
|
x.功德主,
|
|
x.功德類型,
|
|
x.功德名稱,
|
|
x.數量,
|
|
x.金額,
|
|
x.已收,
|
|
x.未收
|
|
})
|
|
.ToList();
|
|
|
|
// 統計資訊
|
|
var summary = new
|
|
{
|
|
總筆數 = exportData.Count,
|
|
總金額 = exportData.Sum(x => x.金額 * x.數量),
|
|
參與法會數 = exportData.Select(x => x.法會ID).Distinct().Count(),
|
|
參與信眾數 = exportData.Select(x => x.信眾編號).Distinct().Count(),
|
|
功德主數量 = exportData.Count(x => x.功德主 == "是")
|
|
};
|
|
|
|
var result = new
|
|
{
|
|
success = true,
|
|
data = new
|
|
{
|
|
details = exportData,
|
|
summary = summary,
|
|
export_time = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"),
|
|
filters = new
|
|
{
|
|
startDate,
|
|
endDate,
|
|
activityNum,
|
|
followerNum
|
|
}
|
|
},
|
|
message = "匯出資料準備完成"
|
|
};
|
|
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest($"匯出失敗:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 取得法會報名統計
|
|
/// GET api/pivot/activity_stats
|
|
/// </summary>
|
|
/// <param name="startDate">開始日期</param>
|
|
/// <param name="endDate">結束日期</param>
|
|
/// <param name="activityNum">法會編號</param>
|
|
/// <returns></returns>
|
|
[HttpGet]
|
|
[Route("api/pivot/activity_stats")]
|
|
public IHttpActionResult GetActivityStats(string startDate = null, string endDate = null, int? activityNum = null)
|
|
{
|
|
try
|
|
{
|
|
var query = from pod in _db.pro_order_detail
|
|
join po in _db.pro_order on pod.order_no equals po.order_no
|
|
join act in _db.activities on po.activity_num equals act.num
|
|
join ai in _db.actItems on pod.actItem_num equals ai.num
|
|
join aik in _db.actItem_kind on ai.kind equals aik.num
|
|
join f in _db.followers on po.f_num equals f.num
|
|
select new
|
|
{
|
|
ActivityNum = act.num,
|
|
ActivityName = act.subject,
|
|
StartDate = act.startDate_solar,
|
|
EndDate = act.endDate_solar,
|
|
FollowerNum = f.num,
|
|
FollowerName = f.u_name,
|
|
OrderNo = po.order_no,
|
|
OrderDate = po.up_time,
|
|
KindName = aik.kind,
|
|
ItemName = ai.subject,
|
|
Qty = pod.qty,
|
|
Price = pod.price,
|
|
Amount = pod.qty * pod.price
|
|
};
|
|
|
|
// 日期篩選
|
|
if (!string.IsNullOrEmpty(startDate) && DateTime.TryParse(startDate, out DateTime start))
|
|
{
|
|
query = query.Where(x => x.StartDate >= start);
|
|
}
|
|
if (!string.IsNullOrEmpty(endDate) && DateTime.TryParse(endDate, out DateTime end))
|
|
{
|
|
query = query.Where(x => x.EndDate <= end);
|
|
}
|
|
|
|
// 法會篩選
|
|
if (activityNum.HasValue)
|
|
{
|
|
query = query.Where(x => x.ActivityNum == activityNum.Value);
|
|
}
|
|
|
|
// 統計資料
|
|
var stats = query.GroupBy(x => new { x.ActivityNum, x.ActivityName, x.StartDate, x.EndDate })
|
|
.Select(g => new
|
|
{
|
|
activity_num = g.Key.ActivityNum,
|
|
activity_name = g.Key.ActivityName,
|
|
start_date = g.Key.StartDate,
|
|
end_date = g.Key.EndDate,
|
|
total_orders = g.Select(x => x.OrderNo).Distinct().Count(),
|
|
total_followers = g.Select(x => x.FollowerNum).Distinct().Count(),
|
|
total_amount = g.Sum(x => x.Amount),
|
|
total_qty = g.Sum(x => x.Qty),
|
|
avg_amount_per_follower = g.GroupBy(x => x.FollowerNum).Average(fg => fg.Sum(x => x.Amount))
|
|
})
|
|
.OrderByDescending(x => x.start_date)
|
|
.ToList();
|
|
|
|
var result = new
|
|
{
|
|
success = true,
|
|
data = stats,
|
|
message = "查詢成功"
|
|
};
|
|
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest($"查詢失敗:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 取得信眾參與分析
|
|
/// GET api/pivot/follower_analysis
|
|
/// </summary>
|
|
/// <param name="startDate">開始日期</param>
|
|
/// <param name="endDate">結束日期</param>
|
|
/// <param name="followerNum">信眾編號</param>
|
|
/// <returns></returns>
|
|
[HttpGet]
|
|
[Route("api/pivot/follower_analysis")]
|
|
public IHttpActionResult GetFollowerAnalysis(string startDate = null, string endDate = null, int? followerNum = null)
|
|
{
|
|
try
|
|
{
|
|
var query = from pod in _db.pro_order_detail
|
|
join po in _db.pro_order on pod.order_no equals po.order_no
|
|
join act in _db.activities on po.activity_num equals act.num
|
|
join ai in _db.actItems on pod.actItem_num equals ai.num
|
|
join aik in _db.actItem_kind on ai.kind equals aik.num
|
|
join f in _db.followers on po.f_num equals f.num
|
|
select new
|
|
{
|
|
FollowerNum = f.num,
|
|
FollowerName = f.u_name,
|
|
FollowerCode = f.f_number,
|
|
ActivityNum = act.num,
|
|
ActivityName = act.subject,
|
|
StartDate = act.startDate_solar,
|
|
EndDate = act.endDate_solar,
|
|
OrderDate = po.up_time,
|
|
KindName = aik.kind,
|
|
ItemName = ai.subject,
|
|
Amount = pod.qty * pod.price,
|
|
HasParent = pod.parent_num != null
|
|
};
|
|
|
|
// 日期篩選
|
|
if (!string.IsNullOrEmpty(startDate) && DateTime.TryParse(startDate, out DateTime start))
|
|
{
|
|
query = query.Where(x => x.StartDate >= start);
|
|
}
|
|
if (!string.IsNullOrEmpty(endDate) && DateTime.TryParse(endDate, out DateTime end))
|
|
{
|
|
query = query.Where(x => x.EndDate <= end);
|
|
}
|
|
|
|
// 信眾篩選
|
|
if (followerNum.HasValue)
|
|
{
|
|
query = query.Where(x => x.FollowerNum == followerNum.Value);
|
|
}
|
|
|
|
// 信眾統計分析
|
|
var followerStats = query.GroupBy(x => new { x.FollowerNum, x.FollowerName, x.FollowerCode })
|
|
.Select(g => new
|
|
{
|
|
follower_num = g.Key.FollowerNum,
|
|
follower_name = g.Key.FollowerName,
|
|
follower_code = g.Key.FollowerCode,
|
|
total_activities = g.Select(x => x.ActivityNum).Distinct().Count(),
|
|
total_amount = g.Sum(x => x.Amount),
|
|
total_orders = g.Select(x => x.OrderDate).Count(),
|
|
avg_amount_per_activity = g.GroupBy(x => x.ActivityNum).Average(ag => ag.Sum(x => x.Amount)),
|
|
is_patron_count = g.Count(x => x.HasParent),
|
|
participation_rate = Math.Round((double)g.Select(x => x.ActivityNum).Distinct().Count() / _db.activities.Count() * 100, 2),
|
|
favorite_kinds = g.GroupBy(x => x.KindName)
|
|
.OrderByDescending(kg => kg.Sum(x => x.Amount))
|
|
.Take(3)
|
|
.Select(kg => new { kind = kg.Key, amount = kg.Sum(x => x.Amount) })
|
|
.ToList(),
|
|
recent_activities = g.OrderByDescending(x => x.OrderDate)
|
|
.Take(5)
|
|
.Select(x => new {
|
|
activity_name = x.ActivityName,
|
|
order_date = x.OrderDate,
|
|
amount = x.Amount
|
|
})
|
|
.ToList()
|
|
})
|
|
.OrderByDescending(x => x.total_amount)
|
|
.ToList();
|
|
|
|
var result = new
|
|
{
|
|
success = true,
|
|
data = followerStats,
|
|
message = "查詢成功"
|
|
};
|
|
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest($"查詢失敗:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 取得收入統計
|
|
/// GET api/pivot/income_stats
|
|
/// </summary>
|
|
/// <param name="startDate">開始日期</param>
|
|
/// <param name="endDate">結束日期</param>
|
|
/// <param name="groupBy">分組方式 (monthly/yearly/activity)</param>
|
|
/// <returns></returns>
|
|
[HttpGet]
|
|
[Route("api/pivot/income_stats")]
|
|
public IHttpActionResult GetIncomeStats(string startDate = null, string endDate = null, string groupBy = "monthly")
|
|
{
|
|
try
|
|
{
|
|
var query = from pod in _db.pro_order_detail
|
|
join po in _db.pro_order on pod.order_no equals po.order_no
|
|
join act in _db.activities on po.activity_num equals act.num
|
|
join ai in _db.actItems on pod.actItem_num equals ai.num
|
|
join aik in _db.actItem_kind on ai.kind equals aik.num
|
|
select new
|
|
{
|
|
ActivityNum = act.num,
|
|
ActivityName = act.subject,
|
|
StartDate = act.startDate_solar,
|
|
OrderDate = po.up_time,
|
|
KindNum = aik.num,
|
|
KindName = aik.kind,
|
|
ItemName = ai.subject,
|
|
Amount = pod.qty * pod.price
|
|
};
|
|
|
|
// 日期篩選
|
|
if (!string.IsNullOrEmpty(startDate) && DateTime.TryParse(startDate, out DateTime start))
|
|
{
|
|
query = query.Where(x => x.StartDate >= start);
|
|
}
|
|
if (!string.IsNullOrEmpty(endDate) && DateTime.TryParse(endDate, out DateTime end))
|
|
{
|
|
query = query.Where(x => x.StartDate <= end);
|
|
}
|
|
|
|
object stats = null;
|
|
|
|
switch (groupBy?.ToLower())
|
|
{
|
|
case "monthly":
|
|
stats = query.GroupBy(x => new {
|
|
Year = x.StartDate.Value.Year,
|
|
Month = x.StartDate.Value.Month
|
|
})
|
|
.Select(g => new
|
|
{
|
|
period = $"{g.Key.Year}/{g.Key.Month:00}",
|
|
year = g.Key.Year,
|
|
month = g.Key.Month,
|
|
total_amount = g.Sum(x => x.Amount),
|
|
total_activities = g.Select(x => x.ActivityNum).Distinct().Count(),
|
|
kind_breakdown = g.GroupBy(x => x.KindName)
|
|
.Select(kg => new {
|
|
kind = kg.Key,
|
|
amount = kg.Sum(x => x.Amount)
|
|
}).ToList()
|
|
})
|
|
.OrderBy(x => x.year).ThenBy(x => x.month)
|
|
.ToList();
|
|
break;
|
|
|
|
case "yearly":
|
|
stats = query.GroupBy(x => x.StartDate.Value.Year)
|
|
.Select(g => new
|
|
{
|
|
period = g.Key.ToString(),
|
|
year = g.Key,
|
|
total_amount = g.Sum(x => x.Amount),
|
|
total_activities = g.Select(x => x.ActivityNum).Distinct().Count(),
|
|
kind_breakdown = g.GroupBy(x => x.KindName)
|
|
.Select(kg => new {
|
|
kind = kg.Key,
|
|
amount = kg.Sum(x => x.Amount)
|
|
}).ToList(),
|
|
monthly_trend = g.GroupBy(x => x.StartDate.Value.Month)
|
|
.Select(mg => new {
|
|
month = mg.Key,
|
|
amount = mg.Sum(x => x.Amount)
|
|
}).OrderBy(x => x.month).ToList()
|
|
})
|
|
.OrderBy(x => x.year)
|
|
.ToList();
|
|
break;
|
|
|
|
case "activity":
|
|
stats = query.GroupBy(x => new { x.ActivityNum, x.ActivityName, x.StartDate })
|
|
.Select(g => new
|
|
{
|
|
activity_num = g.Key.ActivityNum,
|
|
activity_name = g.Key.ActivityName,
|
|
start_date = g.Key.StartDate,
|
|
total_amount = g.Sum(x => x.Amount)
|
|
})
|
|
.OrderByDescending(x => x.start_date)
|
|
.ToList()
|
|
.Select(g => new
|
|
{
|
|
g.activity_num,
|
|
g.activity_name,
|
|
g.start_date,
|
|
g.total_amount,
|
|
kind_breakdown = query.Where(x => x.ActivityNum == g.activity_num)
|
|
.GroupBy(x => x.KindName)
|
|
.Select(kg => new
|
|
{
|
|
kind = kg.Key,
|
|
amount = kg.Sum(x => x.Amount),
|
|
percentage = g.total_amount.HasValue && g.total_amount.Value > 0
|
|
? Math.Round((double)(kg.Sum(x => x.Amount) ?? 0) / (double)g.total_amount.Value * 100, 2)
|
|
: 0
|
|
})
|
|
.OrderByDescending(x => x.amount)
|
|
.ToList()
|
|
})
|
|
.ToList();
|
|
break;
|
|
|
|
default:
|
|
return BadRequest("不支援的分組方式,請使用 monthly, yearly 或 activity");
|
|
}
|
|
|
|
var result = new
|
|
{
|
|
success = true,
|
|
data = stats,
|
|
groupBy = groupBy,
|
|
message = "查詢成功"
|
|
};
|
|
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest($"查詢失敗:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 數據分析 API
|
|
|
|
/// <summary>
|
|
/// 樞紐分析
|
|
/// POST api/pivot/pivot_analysis
|
|
/// </summary>
|
|
/// <param name="request">分析請求參數</param>
|
|
/// <returns></returns>
|
|
[HttpPost]
|
|
[Route("api/pivot/pivot_analysis")]
|
|
public IHttpActionResult PostPivotAnalysis(dynamic request)
|
|
{
|
|
try
|
|
{
|
|
// TODO: 實作樞紐分析邏輯
|
|
var result = new
|
|
{
|
|
success = true,
|
|
data = new
|
|
{
|
|
rows = new List<object>(),
|
|
columns = new List<object>(),
|
|
values = new List<object>()
|
|
},
|
|
message = "分析完成"
|
|
};
|
|
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest($"分析失敗:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 趨勢分析
|
|
/// GET api/pivot/trend_analysis
|
|
/// </summary>
|
|
/// <param name="metric">指標名稱</param>
|
|
/// <param name="startDate">開始日期</param>
|
|
/// <param name="endDate">結束日期</param>
|
|
/// <param name="interval">時間間隔</param>
|
|
/// <returns></returns>
|
|
[HttpGet]
|
|
[Route("api/pivot/trend_analysis")]
|
|
public IHttpActionResult GetTrendAnalysis(string metric, string startDate = null, string endDate = null, string interval = "monthly")
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(metric))
|
|
{
|
|
return BadRequest("請指定分析指標");
|
|
}
|
|
|
|
var query = from pod in _db.pro_order_detail
|
|
join po in _db.pro_order on pod.order_no equals po.order_no
|
|
join act in _db.activities on po.activity_num equals act.num
|
|
join f in _db.followers on po.f_num equals f.num
|
|
select new
|
|
{
|
|
ActivityDate = act.startDate_solar,
|
|
OrderDate = po.up_time,
|
|
Amount = pod.qty * pod.price,
|
|
FollowerNum = f.num,
|
|
ActivityNum = act.num
|
|
};
|
|
|
|
// 日期篩選
|
|
if (!string.IsNullOrEmpty(startDate) && DateTime.TryParse(startDate, out DateTime start))
|
|
{
|
|
query = query.Where(x => x.ActivityDate >= start);
|
|
}
|
|
if (!string.IsNullOrEmpty(endDate) && DateTime.TryParse(endDate, out DateTime end))
|
|
{
|
|
query = query.Where(x => x.ActivityDate <= end);
|
|
}
|
|
|
|
object trendData = null;
|
|
string trendDirection = "stable";
|
|
|
|
switch (metric?.ToLower())
|
|
{
|
|
case "income": // 收入趨勢
|
|
if (interval == "monthly")
|
|
{
|
|
var monthlyIncome = query.GroupBy(x => new {
|
|
Year = x.ActivityDate.Value.Year,
|
|
Month = x.ActivityDate.Value.Month
|
|
})
|
|
.Select(g => new
|
|
{
|
|
period = $"{g.Key.Year}/{g.Key.Month:00}",
|
|
value = g.Sum(x => x.Amount),
|
|
date = new DateTime(g.Key.Year, g.Key.Month, 1)
|
|
})
|
|
.OrderBy(x => x.date)
|
|
.ToList();
|
|
|
|
// 計算趨勢方向
|
|
if (monthlyIncome.Count >= 2)
|
|
{
|
|
var firstHalf = monthlyIncome.Take(monthlyIncome.Count / 2).Average(x => (double)x.value);
|
|
var secondHalf = monthlyIncome.Skip(monthlyIncome.Count / 2).Average(x => (double)x.value);
|
|
trendDirection = secondHalf > firstHalf * 1.05 ? "up" : (secondHalf < firstHalf * 0.95 ? "down" : "stable");
|
|
}
|
|
|
|
trendData = new
|
|
{
|
|
labels = monthlyIncome.Select(x => x.period).ToList(),
|
|
values = monthlyIncome.Select(x => x.value).ToList(),
|
|
growth_rates = monthlyIncome.Select((x, i) => i == 0 ? 0.0 :
|
|
Math.Round((double)(x.value - monthlyIncome[i-1].value) / (double)monthlyIncome[i-1].value * 100, 2))
|
|
.ToList()
|
|
};
|
|
}
|
|
break;
|
|
|
|
case "followers": // 參與信眾數趨勢
|
|
if (interval == "monthly")
|
|
{
|
|
var monthlyFollowers = query.GroupBy(x => new {
|
|
Year = x.ActivityDate.Value.Year,
|
|
Month = x.ActivityDate.Value.Month
|
|
})
|
|
.Select(g => new
|
|
{
|
|
period = $"{g.Key.Year}/{g.Key.Month:00}",
|
|
value = g.Select(x => x.FollowerNum).Distinct().Count(),
|
|
date = new DateTime(g.Key.Year, g.Key.Month, 1)
|
|
})
|
|
.OrderBy(x => x.date)
|
|
.ToList();
|
|
|
|
// 計算趨勢方向
|
|
if (monthlyFollowers.Count >= 2)
|
|
{
|
|
var firstHalf = monthlyFollowers.Take(monthlyFollowers.Count / 2).Average(x => (double)x.value);
|
|
var secondHalf = monthlyFollowers.Skip(monthlyFollowers.Count / 2).Average(x => (double)x.value);
|
|
trendDirection = secondHalf > firstHalf * 1.05 ? "up" : (secondHalf < firstHalf * 0.95 ? "down" : "stable");
|
|
}
|
|
|
|
trendData = new
|
|
{
|
|
labels = monthlyFollowers.Select(x => x.period).ToList(),
|
|
values = monthlyFollowers.Select(x => x.value).ToList(),
|
|
growth_rates = monthlyFollowers.Select((x, i) => i == 0 ? 0.0 :
|
|
Math.Round((double)(x.value - monthlyFollowers[i-1].value) / (double)monthlyFollowers[i-1].value * 100, 2))
|
|
.ToList()
|
|
};
|
|
}
|
|
break;
|
|
|
|
case "activities": // 法會數量趨勢
|
|
var monthlyActivities = query.GroupBy(x => new {
|
|
Year = x.ActivityDate.Value.Year,
|
|
Month = x.ActivityDate.Value.Month
|
|
})
|
|
.Select(g => new
|
|
{
|
|
period = $"{g.Key.Year}/{g.Key.Month:00}",
|
|
value = g.Select(x => x.ActivityNum).Distinct().Count(),
|
|
date = new DateTime(g.Key.Year, g.Key.Month, 1)
|
|
})
|
|
.OrderBy(x => x.date)
|
|
.ToList();
|
|
|
|
trendData = new
|
|
{
|
|
labels = monthlyActivities.Select(x => x.period).ToList(),
|
|
values = monthlyActivities.Select(x => x.value).ToList()
|
|
};
|
|
break;
|
|
|
|
default:
|
|
return BadRequest("不支援的指標,請使用 income, followers 或 activities");
|
|
}
|
|
|
|
var result = new
|
|
{
|
|
success = true,
|
|
data = new
|
|
{
|
|
metric = metric,
|
|
interval = interval,
|
|
trend = trendDirection,
|
|
chart_data = trendData
|
|
},
|
|
message = "分析完成"
|
|
};
|
|
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest($"分析失敗:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 對比分析
|
|
/// GET api/pivot/comparative_analysis
|
|
/// </summary>
|
|
/// <param name="compareType">對比類型</param>
|
|
/// <param name="period1">期間1</param>
|
|
/// <param name="period2">期間2</param>
|
|
/// <returns></returns>
|
|
[HttpGet]
|
|
[Route("api/pivot/comparative_analysis")]
|
|
public IHttpActionResult GetComparativeAnalysis(string compareType, string period1, string period2)
|
|
{
|
|
try
|
|
{
|
|
// TODO: 實作對比分析邏輯
|
|
var result = new
|
|
{
|
|
success = true,
|
|
data = new
|
|
{
|
|
period1_data = new List<object>(),
|
|
period2_data = new List<object>(),
|
|
comparison = new
|
|
{
|
|
growth_rate = 0.0,
|
|
difference = 0.0
|
|
}
|
|
},
|
|
message = "對比完成"
|
|
};
|
|
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest($"對比失敗:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 報表管理 API
|
|
|
|
/// <summary>
|
|
/// 取得自訂報表清單
|
|
/// GET api/pivot/custom_reports
|
|
/// </summary>
|
|
/// <param name="page">頁碼</param>
|
|
/// <param name="pageSize">每頁筆數</param>
|
|
/// <returns></returns>
|
|
[HttpGet]
|
|
[Route("api/pivot/custom_reports")]
|
|
public IHttpActionResult GetCustomReports(int page = 1, int pageSize = 10)
|
|
{
|
|
try
|
|
{
|
|
// TODO: 實作自訂報表查詢邏輯
|
|
var result = new
|
|
{
|
|
success = true,
|
|
data = new
|
|
{
|
|
list = new List<object>(),
|
|
total = 0,
|
|
page = page,
|
|
pageSize = pageSize
|
|
},
|
|
message = "查詢成功"
|
|
};
|
|
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest($"查詢失敗:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 建立自訂報表
|
|
/// POST api/pivot/custom_reports
|
|
/// </summary>
|
|
/// <param name="request">報表設定</param>
|
|
/// <returns></returns>
|
|
[HttpPost]
|
|
[Route("api/pivot/custom_reports")]
|
|
public IHttpActionResult PostCustomReport(dynamic request)
|
|
{
|
|
try
|
|
{
|
|
// TODO: 實作自訂報表建立邏輯
|
|
var result = new
|
|
{
|
|
success = true,
|
|
data = new { id = 1 },
|
|
message = "報表建立成功"
|
|
};
|
|
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest($"建立失敗:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 匯出報表
|
|
/// GET api/pivot/export/{reportId}
|
|
/// </summary>
|
|
/// <param name="reportId">報表編號</param>
|
|
/// <param name="format">匯出格式 (excel/pdf/csv)</param>
|
|
/// <returns></returns>
|
|
[HttpGet]
|
|
[Route("api/pivot/export/{reportId}")]
|
|
public IHttpActionResult GetReportExport(int reportId, string format = "excel")
|
|
{
|
|
try
|
|
{
|
|
// TODO: 實作報表匯出邏輯
|
|
var result = new
|
|
{
|
|
success = true,
|
|
data = new
|
|
{
|
|
download_url = $"/admin/pivot/downloads/report_{reportId}.{format}",
|
|
file_name = $"report_{reportId}_{DateTime.Now:yyyyMMdd}.{format}"
|
|
},
|
|
message = "匯出成功"
|
|
};
|
|
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest($"匯出失敗:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Excel 數據連接 API
|
|
|
|
/// <summary>
|
|
/// Excel 數據連接專用 - 簡化格式
|
|
/// GET api/pivot/excel_data
|
|
/// </summary>
|
|
/// <param name="format">回傳格式 (json/csv)</param>
|
|
/// <param name="limit">限制筆數</param>
|
|
/// <returns></returns>
|
|
[HttpGet]
|
|
[Route("api/pivot/excel_data")]
|
|
[AllowAnonymous] // 允許 Excel 直接呼叫
|
|
public IHttpActionResult GetExcelData(string format = "json", int limit = 5000)
|
|
{
|
|
try
|
|
{
|
|
// 直接查詢,對應視圖結構
|
|
var query = from pod in _db.pro_order_detail
|
|
join po in _db.pro_order on pod.order_no equals po.order_no
|
|
join ai in _db.actItems on pod.actItem_num equals ai.num
|
|
join act in _db.activities on po.activity_num equals act.num
|
|
join f in _db.followers on po.f_num equals f.num
|
|
join aik in _db.actItem_kind on ai.kind equals aik.num
|
|
orderby po.up_time descending, act.num descending
|
|
select new
|
|
{
|
|
ActivityId = act.num,
|
|
ActivityName = act.subject,
|
|
StartDate = act.startDate_solar,
|
|
EndDate = act.endDate_solar,
|
|
FollowerCode = f.f_number,
|
|
FollowerName = f.u_name,
|
|
OrderNo = po.order_no,
|
|
OrderDate = po.up_time,
|
|
IsPatron = pod.parent_num != null ? "是" : "否",
|
|
KindName = aik.kind,
|
|
ItemName = ai.subject,
|
|
Quantity = pod.qty,
|
|
Price = pod.price,
|
|
Received = 0,
|
|
Outstanding = pod.price * pod.qty
|
|
};
|
|
|
|
var data = query.Take(limit).ToList();
|
|
|
|
if (format?.ToLower() == "csv")
|
|
{
|
|
// CSV 格式回傳
|
|
var csv = "法會ID,法會名稱,開始日期,結束日期,信眾編號,信眾姓名,報名編號,報名日期,功德主,功德類型,功德名稱,數量,金額,已收,未收\n";
|
|
foreach (var item in data)
|
|
{
|
|
csv += $"{item.ActivityId},{item.ActivityName},{item.StartDate:yyyy/MM/dd},{item.EndDate:yyyy/MM/dd}," +
|
|
$"{item.FollowerCode},{item.FollowerName},{item.OrderNo},{item.OrderDate:yyyy/MM/dd HH:mm}," +
|
|
$"{item.IsPatron},{item.KindName},{item.ItemName},{item.Quantity},{item.Price},{item.Received},{item.Outstanding}\n";
|
|
}
|
|
|
|
return Ok(csv);
|
|
}
|
|
|
|
// JSON 格式回傳 (預設)
|
|
var result = new
|
|
{
|
|
success = true,
|
|
data = data,
|
|
count = data.Count,
|
|
generated_at = DateTime.Now,
|
|
message = "資料查詢成功"
|
|
};
|
|
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest($"資料查詢失敗:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Excel 數據連接專用 - 按欄位顏色分類格式
|
|
/// GET api/pivot/excel_data_structured
|
|
/// </summary>
|
|
/// <param name="includeMetadata">是否包含元數據</param>
|
|
/// <param name="limit">限制筆數</param>
|
|
/// <returns></returns>
|
|
[HttpGet]
|
|
[Route("api/pivot/excel_data_structured")]
|
|
[AllowAnonymous]
|
|
public IHttpActionResult GetExcelDataStructured(bool includeMetadata = false, int limit = 5000)
|
|
{
|
|
try
|
|
{
|
|
var query = from pod in _db.pro_order_detail
|
|
join po in _db.pro_order on pod.order_no equals po.order_no
|
|
join ai in _db.actItems on pod.actItem_num equals ai.num
|
|
join act in _db.activities on po.activity_num equals act.num
|
|
join f in _db.followers on po.f_num equals f.num
|
|
join aik in _db.actItem_kind on ai.kind equals aik.num
|
|
orderby po.up_time descending, act.num descending
|
|
select new
|
|
{
|
|
// 🟠 橘色欄位 - 法會基礎資料
|
|
activity_fields = new
|
|
{
|
|
法會ID = act.num,
|
|
法會名稱 = act.subject,
|
|
開始日期 = act.startDate_solar,
|
|
結束日期 = act.endDate_solar
|
|
},
|
|
|
|
// 🔵 藍色欄位 - 信眾基礎資料
|
|
follower_fields = new
|
|
{
|
|
信眾編號 = f.f_number,
|
|
信眾姓名 = f.u_name,
|
|
報名編號 = po.order_no,
|
|
報名日期 = po.up_time
|
|
},
|
|
|
|
// 🟢 綠色欄位 - 功德資訊欄位
|
|
merit_info_fields = new
|
|
{
|
|
功德主 = pod.parent_num != null ? "是" : "否",
|
|
功德類型 = aik.kind,
|
|
功德名稱 = ai.subject
|
|
},
|
|
|
|
// 🟣 紫色欄位 - 計算欄位
|
|
calculated_fields = new
|
|
{
|
|
數量 = pod.qty,
|
|
金額 = pod.price,
|
|
已收 = 0,
|
|
未收 = pod.price * pod.qty,
|
|
小計 = pod.price * pod.qty
|
|
},
|
|
|
|
// 📊 元數據 (可選)
|
|
metadata = includeMetadata ? new
|
|
{
|
|
原始資料 = new
|
|
{
|
|
parent_num = pod.parent_num,
|
|
kind_num = aik.num,
|
|
item_num = ai.num,
|
|
activity_num = act.num,
|
|
follower_num = f.num
|
|
},
|
|
欄位分類說明 = new
|
|
{
|
|
橘色欄位_法會資料 = "法會相關的基礎資料,用於法會維度分析",
|
|
藍色欄位_信眾資料 = "信眾相關的基礎資料,用於信眾維度分析",
|
|
綠色欄位_功德資訊 = "功德相關的資訊欄位,包含功德主、功德類型和名稱",
|
|
紫色欄位_計算欄位 = "需要數學計算或統計的欄位"
|
|
}
|
|
} : null
|
|
};
|
|
|
|
var data = query.Take(limit).ToList();
|
|
|
|
var result = new
|
|
{
|
|
success = true,
|
|
data = new
|
|
{
|
|
records = data,
|
|
field_categories = new
|
|
{
|
|
橘色_法會基礎資料 = new[] { "法會ID", "法會名稱", "開始日期", "結束日期" },
|
|
藍色_信眾基礎資料 = new[] { "信眾編號", "信眾姓名", "報名編號", "報名日期" },
|
|
綠色_功德資訊欄位 = new[] { "功德主", "功德類型", "功德名稱" },
|
|
紫色_計算欄位 = new[] { "數量", "金額", "已收", "未收" }
|
|
},
|
|
usage_notes = new
|
|
{
|
|
橘色欄位說明 = "法會相關基礎資料,適合用於法會維度的篩選和分組",
|
|
藍色欄位說明 = "信眾相關基礎資料,適合用於信眾維度的篩選和分組",
|
|
綠色欄位說明 = "功德資訊欄位,包含功德主判斷、功德類型和名稱,適合用於功德維度的篩選和分組",
|
|
紫色欄位說明 = "計算統計欄位,適合用於樞紐分析的值區域進行加總",
|
|
Excel樞紐分析建議 = new
|
|
{
|
|
篩選器 = "橘色(法會名稱) + 藍色(報名日期) + 綠色(功德類型)",
|
|
列標籤 = "藍色(信眾姓名) + 綠色(功德主)",
|
|
欄標籤 = "橘色(法會名稱) 或時間維度",
|
|
值區域 = "紫色(金額加總、數量計數)"
|
|
}
|
|
}
|
|
},
|
|
count = data.Count,
|
|
generated_at = DateTime.Now,
|
|
message = "結構化資料查詢成功"
|
|
};
|
|
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest($"查詢失敗:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 工具方法
|
|
|
|
/// <summary>
|
|
/// 釋放資源
|
|
/// </summary>
|
|
/// <param name="disposing"></param>
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
_db?.Dispose();
|
|
}
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
#endregion
|
|
} |