update new version
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert'; // [修正 1] 必須導入此包才能使用 json.decode
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:intl/intl.dart';
|
||||
import '../services/generic_api_service.dart';
|
||||
import '../auth_manager.dart';
|
||||
import 'expense_model.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class ExpenseApiService {
|
||||
final GenericApiService _apiService = GenericApiService();
|
||||
|
||||
// [新增] 獲取費用類別清單 (供下拉選單使用)
|
||||
Future<List<ExpenseClass>> fetchExpenseClasses() async {
|
||||
return await _apiService.fetchList<ExpenseClass>(
|
||||
tableName: "acc_ExpClass",
|
||||
pk: "ExpId",
|
||||
queryFilter: "1^100^ExpId^*^", // 取得全部分類
|
||||
fromJson: (json) => ExpenseClass.fromJson(json),
|
||||
);
|
||||
}
|
||||
|
||||
// 1. 獲取費用申請清單
|
||||
Future<List<ExpenseApply>> fetchExpenses(String empNo) async {
|
||||
// 根據 index.js 邏輯,queryFilter 應符合後端 split('^') 的期待
|
||||
String queryFilter = "1^100^ApplyDate^*^EmpNo^$empNo";
|
||||
return await _apiService.fetchList<ExpenseApply>(
|
||||
tableName: "acc_ExpsApply",
|
||||
pk: "ApplyId",
|
||||
queryFilter: queryFilter,
|
||||
fromJson: (json) => ExpenseApply.fromJson(json),
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 上傳照片
|
||||
/// 對接 index.js 的 upload.single('file')
|
||||
Future<String?> uploadImage(File file) async {
|
||||
try {
|
||||
// 使用 GenericApiService 內定義的 BASE_IP
|
||||
var request = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse("$BASE_IP/upload/EIS_demo/images")
|
||||
);
|
||||
|
||||
// 取得本地檔案名稱
|
||||
String picFileName = "$BASE_IP/upload/EIS_demo/images/" + file.path.split('/').last;
|
||||
|
||||
// [修正] index.js 的 multer 配置要求 key 必須是 'file'
|
||||
request.files.add(await http.MultipartFile.fromPath('file', file.path));
|
||||
|
||||
var streamedResponse = await request.send();
|
||||
var response = await http.Response.fromStream(streamedResponse);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
//final Map<String, dynamic> resp = json.decode(response.body);
|
||||
|
||||
// [修正] 對接 index.js: res.json({ code: 0, data: { filename: "..." } })
|
||||
/*
|
||||
final int code = resp['code'] ?? resp['Code'] ?? -1;
|
||||
if (code == 0 && resp['data'] != null) {
|
||||
return resp['data']['filename']; // 取得後端產生的新檔名
|
||||
} else {
|
||||
print("後端上傳錯誤: ${resp['msg']}");
|
||||
}
|
||||
*/
|
||||
return picFileName; // 這是回傳後端產生的檔名
|
||||
}
|
||||
} catch (e) {
|
||||
print("圖片上傳異常: $e");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 提交單據
|
||||
Future<bool> submitExpense(ExpenseApply expense, String uploadedFileName) async {
|
||||
// [修正] 確保 AuthManager 的調用與專案一致
|
||||
final String? uid = AuthManager().currentUserId;
|
||||
final String currentUid = uid ?? expense.empNo;
|
||||
final String now = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
|
||||
|
||||
// 建立要存入資料庫的純字串資料 Map
|
||||
// 這些 Key 會被 index.js 的 ORM 邏輯自動轉為 SQL 欄位
|
||||
final Map<String, String> data = {
|
||||
"ApplyDate": now,
|
||||
"EmpNo": currentUid,
|
||||
"ExpId": expense.expId ?? "", // [新增] 寫入類別 ID
|
||||
"ExpAmt": expense.expAmt?.toString() ?? "0",
|
||||
"RowData": expense.rowData ?? "",
|
||||
"PicPath": uploadedFileName, // 儲存 uploadImage 回傳的檔名
|
||||
"CreatorId": currentUid,
|
||||
"CreateDateTime": now,
|
||||
};
|
||||
|
||||
// 呼叫 GenericApiService
|
||||
final result = await _apiService.fetchList<ExpenseApply>(
|
||||
tableName: "acc_ExpsApply",
|
||||
pk: "ApplyId",
|
||||
queryFilter: "", // 新增單據時,filter 通常為空字串
|
||||
action: "C", // [關鍵] 指定動作為 Create
|
||||
data: data, // 傳遞資料
|
||||
fromJson: (json) => ExpenseApply.fromJson(json),
|
||||
);
|
||||
|
||||
// 只要有回傳資料,代表資料庫 Insert 成功並回傳了該筆資料
|
||||
return result.isNotEmpty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import './expense_model.dart';
|
||||
|
||||
class ExpenseDetail extends StatelessWidget {
|
||||
final ExpenseApply expense;
|
||||
|
||||
const ExpenseDetail({required this.expense, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('費用申請詳情'),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
elevation: 0.5,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. 頂部狀態與摘要區塊
|
||||
_buildHeaderStatus(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 2. 主要資訊卡片 (日期、工號、申請人)
|
||||
_buildInfoCard(context),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 3. 費用說明區塊 (RowData)
|
||||
const Text(
|
||||
'費用說明',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blueGrey),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
expense.rowData ?? '無說明內容',
|
||||
style: const TextStyle(fontSize: 15, height: 1.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 4. 單據照片預覽區塊 (PicPath)
|
||||
const Text(
|
||||
'單據照片',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blueGrey),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildImagePreview(context),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 頂部狀態與 ID 顯示
|
||||
Widget _buildHeaderStatus() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'單號: ${expense.applyId ?? 'N/A'}',
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'費用報支申請',
|
||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 假設狀態固定為已提交 (可依需求擴充資料表狀態欄位)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Text(
|
||||
'已提交',
|
||||
style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 核心資訊卡片
|
||||
Widget _buildInfoCard(BuildContext context) {
|
||||
final df = DateFormat('yyyy-MM-dd HH:mm');
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildDetailRow(Icons.event, '費用日期', DateFormat('yyyy-MM-dd').format(expense.applyDate)),
|
||||
const Divider(height: 30),
|
||||
_buildDetailRow(Icons.badge_outlined, '申請人工號', expense.empNo),
|
||||
const Divider(height: 30),
|
||||
_buildDetailRow(Icons.category, '費用類別', expense.expCName ?? expense.expId ?? '未分類'),
|
||||
const Divider(height: 30),
|
||||
_buildDetailRow(Icons.monetization_on, '申請金額', '${expense.expAmt?.toStringAsFixed(2)}'),
|
||||
const Divider(height: 30),
|
||||
_buildDetailRow(Icons.category, '費用說明', expense.rowData ?? ''),
|
||||
const Divider(height: 30),
|
||||
_buildDetailRow(Icons.person_pin, '建立者', expense.creatorId ?? '系統'),
|
||||
const Divider(height: 30),
|
||||
_buildDetailRow(Icons.history, '系統存檔時間',
|
||||
expense.createDateTime != null ? df.format(expense.createDateTime!) : 'N/A'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 輔助元件:建立細節列
|
||||
Widget _buildDetailRow(IconData icon, String label, String value) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: Colors.blueGrey.shade400),
|
||||
const SizedBox(width: 12),
|
||||
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
const Spacer(),
|
||||
Text(value, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 圖片預覽元件
|
||||
Widget _buildImagePreview(BuildContext context) {
|
||||
if (expense.picPath == null || expense.picPath!.isEmpty) {
|
||||
return Container(
|
||||
height: 150,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Center(child: Text('無單據照片', style: TextStyle(color: Colors.grey))),
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _showFullScreenImage(context),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
color: Colors.grey.shade200,
|
||||
child: Image.network(
|
||||
expense.fullPicUrl,
|
||||
height: 250,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
// 處理圖片加載失敗
|
||||
errorBuilder: (context, error, stackTrace) => Container(
|
||||
height: 200,
|
||||
child: const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.broken_image, color: Colors.grey, size: 40),
|
||||
Text('無法載入單據圖片', style: TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 全螢幕查看圖片
|
||||
void _showFullScreenImage(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(backgroundColor: Colors.black, foregroundColor: Colors.white),
|
||||
body: Center(
|
||||
child: InteractiveViewer( // 支援手勢縮放
|
||||
child: Image.network(expense.fullPicUrl),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import './expense_api.dart';
|
||||
import 'expense_model.dart';
|
||||
import '../services/ui_utils.dart';
|
||||
|
||||
class ExpenseForm extends StatefulWidget {
|
||||
final String userId;
|
||||
const ExpenseForm({required this.userId, super.key});
|
||||
|
||||
@override
|
||||
State<ExpenseForm> createState() => _ExpenseFormState();
|
||||
}
|
||||
|
||||
class _ExpenseFormState extends State<ExpenseForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _apiService = ExpenseApiService();
|
||||
final _descController = TextEditingController();
|
||||
final _amtController = TextEditingController(); // [新增] 金額控制器
|
||||
|
||||
File? _image;
|
||||
bool _isSubmitting = false;
|
||||
// New
|
||||
String? _selectedExpId;
|
||||
List<ExpenseClass> _classList = [];
|
||||
bool _isLoadingClass = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadClasses();
|
||||
}
|
||||
|
||||
// 讀取分類資料
|
||||
void _loadClasses() async {
|
||||
final list = await _apiService.fetchExpenseClasses();
|
||||
setState(() {
|
||||
_classList = list;
|
||||
_isLoadingClass = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 啟動相機
|
||||
Future<void> _takePhoto() async {
|
||||
final picker = ImagePicker();
|
||||
final pickedFile = await picker.pickImage(source: ImageSource.camera, imageQuality: 70);
|
||||
if (pickedFile != null) {
|
||||
setState(() => _image = File(pickedFile.path));
|
||||
}
|
||||
}
|
||||
|
||||
void _submit() async {
|
||||
// 1. 基本驗證
|
||||
if (!_formKey.currentState!.validate() || _image == null) {
|
||||
UiUtils.showMsg(context, "請輸入說明並拍攝單據", isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
|
||||
try {
|
||||
// 2. 第一步:上傳照片
|
||||
String? fileName = await _apiService.uploadImage(_image!);
|
||||
|
||||
if (fileName == null) {
|
||||
if (mounted) UiUtils.showMsg(context, "圖片上傳失敗,請檢查網路連接", isError: true);
|
||||
setState(() => _isSubmitting = false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 第二步:提交資料表紀錄
|
||||
final newExpense = ExpenseApply(
|
||||
applyDate: DateTime.now(),
|
||||
empNo: widget.userId,
|
||||
rowData: _descController.text,
|
||||
expId: _selectedExpId,
|
||||
expAmt: int.tryParse(_amtController.text),
|
||||
);
|
||||
|
||||
// 這裡會呼叫 fetchList,如果上面 GenericApiService 修正了,這裡就會回傳 true
|
||||
bool success = await _apiService.submitExpense(newExpense, fileName);
|
||||
|
||||
if (success) {
|
||||
if (mounted) {
|
||||
UiUtils.showMsg(context, "申請成功!");
|
||||
// 延遲一小段時間讓使用者看到訊息再關閉,或直接關閉
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (mounted) Navigator.pop(context, true);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (mounted) UiUtils.showMsg(context, "資料寫入失敗,請確認伺服器回應", isError: true);
|
||||
setState(() => _isSubmitting = false);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) UiUtils.showMsg(context, "發生非預期錯誤: $e", isError: true);
|
||||
setState(() => _isSubmitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('新增費用申請')),
|
||||
body: _isSubmitting
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: _takePhoto,
|
||||
child: Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey),
|
||||
),
|
||||
child: _image == null
|
||||
? const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [Icon(Icons.camera_alt, size: 50), Text("點擊拍照收據")],
|
||||
)
|
||||
: Image.file(_image!, fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
// [新增] 類別下拉選單
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedExpId,
|
||||
decoration: const InputDecoration(labelText: "費用類別", border: OutlineInputBorder()),
|
||||
items: _classList.map((c) => DropdownMenuItem(
|
||||
value: c.expId,
|
||||
child: Text(c.expCName ?? c.expId),
|
||||
)).toList(),
|
||||
onChanged: (val) => setState(() => _selectedExpId = val),
|
||||
validator: (v) => v == null ? "請選擇類別" : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 2. [新增] 金額輸入框
|
||||
TextFormField(
|
||||
controller: _amtController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "費用金額 (ExpAmt)",
|
||||
prefixText: "",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return "請輸入金額";
|
||||
if (double.tryParse(v) == null) return "請輸入有效的數字";
|
||||
if (double.parse(v) <= 0) return "金額必須大於 0";
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _descController,
|
||||
decoration: const InputDecoration(labelText: "費用說明 (RowData)", border: OutlineInputBorder()),
|
||||
maxLines: 3,
|
||||
validator: (v) => v!.isEmpty ? "必填" : null,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(minimumSize: const Size(double.infinity, 50)),
|
||||
child: const Text("提交申請"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'expense_model.dart';
|
||||
import './expense_api.dart';
|
||||
import './expense_form.dart';
|
||||
import './expense_detail.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class ExpenseManager extends StatefulWidget {
|
||||
final String currentUserId;
|
||||
const ExpenseManager({required this.currentUserId, super.key});
|
||||
|
||||
@override
|
||||
State<ExpenseManager> createState() => _ExpenseManagerState();
|
||||
}
|
||||
|
||||
class _ExpenseManagerState extends State<ExpenseManager> {
|
||||
final ExpenseApiService _apiService = ExpenseApiService();
|
||||
late Future<List<ExpenseApply>> _expenseFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_refreshList();
|
||||
}
|
||||
|
||||
void _refreshList() {
|
||||
setState(() {
|
||||
_expenseFuture = _apiService.fetchExpenses(widget.currentUserId);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('費用申請紀錄')),
|
||||
body: FutureBuilder<List<ExpenseApply>>(
|
||||
future: _expenseFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
|
||||
if (!snapshot.hasData || snapshot.data!.isEmpty) return const Center(child: Text('查無申請紀錄'));
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: snapshot.data!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = snapshot.data![index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.receipt_long, color: Colors.blue),
|
||||
title: Text(item.rowData ?? "無說明"),
|
||||
subtitle: Text("金額: ${item.expAmt} | 日期: ${DateFormat('yyyy-MM-dd').format(item.applyDate)}"),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => ExpenseDetail(expense: item))),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
icon: const Icon(Icons.add_a_photo),
|
||||
label: const Text("申請費用"),
|
||||
onPressed: () async {
|
||||
final result = await Navigator.push(context, MaterialPageRoute(builder: (context) => ExpenseForm(userId: widget.currentUserId)));
|
||||
if (result == true) _refreshList();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
// --- 新增類別模型 ---
|
||||
class ExpenseClass {
|
||||
final String expId;
|
||||
final String? expCName;
|
||||
final String? expDesc;
|
||||
|
||||
ExpenseClass({required this.expId, this.expCName, this.expDesc});
|
||||
|
||||
factory ExpenseClass.fromJson(Map<String, dynamic> json) {
|
||||
return ExpenseClass(
|
||||
expId: json['ExpId'] ?? '',
|
||||
expCName: json['ExpCName'],
|
||||
expDesc: json['Exp_Desc'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExpenseApply {
|
||||
final int? applyId;
|
||||
final DateTime applyDate;
|
||||
final String empNo;
|
||||
final String? rowData; // 費用說明
|
||||
final String? picPath; // 圖片檔名/路徑
|
||||
final String? expId; // [新增] 類別 ID
|
||||
final String? expCName; // [新增] 類別名稱 (供查詢顯示用)
|
||||
final int? expAmt; // [新增] 費用金額
|
||||
final String? creatorId;
|
||||
final DateTime? createDateTime;
|
||||
|
||||
ExpenseApply({
|
||||
this.applyId,
|
||||
required this.applyDate,
|
||||
required this.empNo,
|
||||
this.rowData,
|
||||
this.picPath,
|
||||
this.expId,
|
||||
this.expCName,
|
||||
this.expAmt,
|
||||
this.creatorId,
|
||||
this.createDateTime,
|
||||
});
|
||||
|
||||
factory ExpenseApply.fromJson(Map<String, dynamic> json) {
|
||||
return ExpenseApply(
|
||||
applyId: int.tryParse(json['ApplyId']?.toString() ?? ''),
|
||||
applyDate: json['ApplyDate'] != null ? DateTime.parse(json['ApplyDate']) : DateTime.now(),
|
||||
empNo: json['EmpNo'] as String? ?? '',
|
||||
rowData: json['RowData'] as String?,
|
||||
picPath: json['PicPath'] as String?,
|
||||
expId: json['ExpId'] as String? ?? '', // [新增]
|
||||
expCName: json['ExpCName'] as String? ?? '', // [新增] 假設後端透過 View 聯集查詢
|
||||
expAmt: int.tryParse(json['ExpAmt']?.toString() ?? '0') ?? 0,
|
||||
creatorId: json['CreatorId'] as String?,
|
||||
createDateTime: json['CreateDateTime'] != null ? DateTime.parse(json['CreateDateTime']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
// 輔助屬性:取得完整圖片路徑 (假設後端基礎 URL)
|
||||
String get fullPicUrl => "https://api.gex.com.tw:8033/uploads/$picPath";
|
||||
}
|
||||
Reference in New Issue
Block a user