progress in import persons
This commit is contained in:
parent
b8ab020754
commit
c31240846f
|
|
@ -1,4 +1,5 @@
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Body
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Body
|
||||||
|
from fastapi import UploadFile, File
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
|
|
@ -493,6 +494,70 @@ async def export_persons_pdf(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/businesses/{business_id}/persons/import/template",
|
||||||
|
summary="دانلود تمپلیت ایمپورت اشخاص",
|
||||||
|
description="فایل Excel تمپلیت برای ایمپورت اشخاص را برمیگرداند",
|
||||||
|
)
|
||||||
|
async def download_persons_import_template(
|
||||||
|
business_id: int,
|
||||||
|
request: Request,
|
||||||
|
auth_context: AuthContext = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
import io
|
||||||
|
import datetime
|
||||||
|
from fastapi.responses import Response
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, Alignment
|
||||||
|
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "Template"
|
||||||
|
|
||||||
|
headers = [
|
||||||
|
'code','alias_name','first_name','last_name','person_type','person_types','company_name','payment_id',
|
||||||
|
'national_id','registration_number','economic_id','country','province','city','address','postal_code',
|
||||||
|
'phone','mobile','fax','email','website','share_count','commission_sale_percent','commission_sales_return_percent',
|
||||||
|
'commission_sales_amount','commission_sales_return_amount'
|
||||||
|
]
|
||||||
|
for col, header in enumerate(headers, 1):
|
||||||
|
cell = ws.cell(row=1, column=col, value=header)
|
||||||
|
cell.font = Font(bold=True)
|
||||||
|
cell.alignment = Alignment(horizontal="center")
|
||||||
|
|
||||||
|
# Sample row
|
||||||
|
sample = [
|
||||||
|
'', 'نمونه نام مستعار', 'علی', 'احمدی', 'مشتری', 'مشتری, فروشنده', 'نمونه شرکت', 'PID123',
|
||||||
|
'0012345678', '12345', 'ECO-1', 'ایران', 'تهران', 'تهران', 'خیابان مثال ۱', '1234567890',
|
||||||
|
'02112345678', '09120000000', '', 'test@example.com', 'example.com', '', '5', '0', '0', '0'
|
||||||
|
]
|
||||||
|
for col, val in enumerate(sample, 1):
|
||||||
|
ws.cell(row=2, column=col, value=val)
|
||||||
|
|
||||||
|
# Auto width
|
||||||
|
for column in ws.columns:
|
||||||
|
try:
|
||||||
|
letter = column[0].column_letter
|
||||||
|
max_len = max(len(str(c.value)) if c.value is not None else 0 for c in column)
|
||||||
|
ws.column_dimensions[letter].width = min(max_len + 2, 50)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
buf = io.BytesIO()
|
||||||
|
wb.save(buf)
|
||||||
|
buf.seek(0)
|
||||||
|
|
||||||
|
filename = f"persons_import_template_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||||
|
return Response(
|
||||||
|
content=buf.getvalue(),
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f"attachment; filename={filename}",
|
||||||
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/persons/{person_id}",
|
@router.get("/persons/{person_id}",
|
||||||
summary="جزئیات شخص",
|
summary="جزئیات شخص",
|
||||||
description="دریافت جزئیات یک شخص",
|
description="دریافت جزئیات یک شخص",
|
||||||
|
|
@ -626,3 +691,158 @@ async def get_persons_summary_endpoint(
|
||||||
request=request,
|
request=request,
|
||||||
message="خلاصه اشخاص با موفقیت دریافت شد",
|
message="خلاصه اشخاص با موفقیت دریافت شد",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/businesses/{business_id}/persons/import/excel",
|
||||||
|
summary="ایمپورت اشخاص از فایل Excel",
|
||||||
|
description="فایل اکسل را دریافت میکند و بهصورت dry-run یا واقعی پردازش میکند",
|
||||||
|
)
|
||||||
|
async def import_persons_excel(
|
||||||
|
business_id: int,
|
||||||
|
request: Request,
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
dry_run: bool = Body(default=True),
|
||||||
|
match_by: str = Body(default="code"),
|
||||||
|
conflict_policy: str = Body(default="upsert"),
|
||||||
|
auth_context: AuthContext = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
if not file.filename or not file.filename.lower().endswith('.xlsx'):
|
||||||
|
raise HTTPException(status_code=400, detail="فرمت فایل معتبر نیست. تنها xlsx پشتیبانی میشود")
|
||||||
|
|
||||||
|
content = await file.read()
|
||||||
|
try:
|
||||||
|
wb = load_workbook(filename=io.BytesIO(content), data_only=True)
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(status_code=400, detail="امکان خواندن فایل وجود ندارد")
|
||||||
|
|
||||||
|
ws = wb.active
|
||||||
|
rows = list(ws.iter_rows(values_only=True))
|
||||||
|
if not rows:
|
||||||
|
return success_response(data={"summary": {"total": 0}}, request=request, message="فایل خالی است")
|
||||||
|
|
||||||
|
headers = [str(h).strip() if h is not None else "" for h in rows[0]]
|
||||||
|
data_rows = rows[1:]
|
||||||
|
|
||||||
|
# helper to map enum strings (fa/en) to internal value
|
||||||
|
def normalize_person_type(value: str) -> Optional[str]:
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
value = str(value).strip()
|
||||||
|
mapping = {
|
||||||
|
'customer': 'مشتری', 'marketer': 'بازاریاب', 'employee': 'کارمند', 'supplier': 'تامینکننده',
|
||||||
|
'partner': 'همکار', 'seller': 'فروشنده', 'shareholder': 'سهامدار'
|
||||||
|
}
|
||||||
|
for en, fa in mapping.items():
|
||||||
|
if value.lower() == en or value == fa:
|
||||||
|
return fa
|
||||||
|
return value # assume already fa
|
||||||
|
|
||||||
|
errors: list[dict] = []
|
||||||
|
valid_items: list[dict] = []
|
||||||
|
|
||||||
|
for idx, row in enumerate(data_rows, start=2):
|
||||||
|
item: dict[str, Any] = {}
|
||||||
|
row_errors: list[str] = []
|
||||||
|
for ci, key in enumerate(headers):
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
val = row[ci] if ci < len(row) else None
|
||||||
|
if isinstance(val, str):
|
||||||
|
val = val.strip()
|
||||||
|
item[key] = val
|
||||||
|
# normalize types
|
||||||
|
if 'person_type' in item and item['person_type']:
|
||||||
|
item['person_type'] = normalize_person_type(item['person_type'])
|
||||||
|
if 'person_types' in item and item['person_types']:
|
||||||
|
# split by comma
|
||||||
|
parts = [normalize_person_type(p.strip()) for p in str(item['person_types']).split(',') if str(p).strip()]
|
||||||
|
item['person_types'] = parts
|
||||||
|
|
||||||
|
# alias_name required
|
||||||
|
if not item.get('alias_name'):
|
||||||
|
row_errors.append('alias_name الزامی است')
|
||||||
|
|
||||||
|
# shareholder rule
|
||||||
|
if (item.get('person_type') == 'سهامدار') or (isinstance(item.get('person_types'), list) and 'سهامدار' in item.get('person_types', [])):
|
||||||
|
sc = item.get('share_count')
|
||||||
|
try:
|
||||||
|
sc_val = int(sc) if sc is not None and str(sc).strip() != '' else None
|
||||||
|
except Exception:
|
||||||
|
sc_val = None
|
||||||
|
if sc_val is None or sc_val <= 0:
|
||||||
|
row_errors.append('برای سهامدار share_count باید > 0 باشد')
|
||||||
|
else:
|
||||||
|
item['share_count'] = sc_val
|
||||||
|
|
||||||
|
if row_errors:
|
||||||
|
errors.append({"row": idx, "errors": row_errors})
|
||||||
|
continue
|
||||||
|
|
||||||
|
valid_items.append(item)
|
||||||
|
|
||||||
|
inserted = 0
|
||||||
|
updated = 0
|
||||||
|
skipped = 0
|
||||||
|
|
||||||
|
if not dry_run and valid_items:
|
||||||
|
# apply import with conflict policy
|
||||||
|
from adapters.db.models.person import Person
|
||||||
|
from sqlalchemy import and_
|
||||||
|
|
||||||
|
def find_existing(session: Session, data: dict) -> Optional[Person]:
|
||||||
|
if match_by == 'national_id' and data.get('national_id'):
|
||||||
|
return session.query(Person).filter(and_(Person.business_id == business_id, Person.national_id == data['national_id'])).first()
|
||||||
|
if match_by == 'email' and data.get('email'):
|
||||||
|
return session.query(Person).filter(and_(Person.business_id == business_id, Person.email == data['email'])).first()
|
||||||
|
if match_by == 'code' and data.get('code'):
|
||||||
|
try:
|
||||||
|
code_int = int(data['code'])
|
||||||
|
return session.query(Person).filter(and_(Person.business_id == business_id, Person.code == code_int)).first()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
for data in valid_items:
|
||||||
|
existing = find_existing(db, data)
|
||||||
|
if existing is None:
|
||||||
|
# create
|
||||||
|
try:
|
||||||
|
create_person(db, business_id, PersonCreateRequest(**data))
|
||||||
|
inserted += 1
|
||||||
|
except Exception:
|
||||||
|
skipped += 1
|
||||||
|
else:
|
||||||
|
if conflict_policy == 'insert':
|
||||||
|
skipped += 1
|
||||||
|
elif conflict_policy in ('update', 'upsert'):
|
||||||
|
try:
|
||||||
|
update_person(db, existing.id, business_id, PersonUpdateRequest(**data))
|
||||||
|
updated += 1
|
||||||
|
except Exception:
|
||||||
|
skipped += 1
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"total": len(data_rows),
|
||||||
|
"valid": len(valid_items),
|
||||||
|
"invalid": len(errors),
|
||||||
|
"inserted": inserted,
|
||||||
|
"updated": updated,
|
||||||
|
"skipped": skipped,
|
||||||
|
"dry_run": dry_run,
|
||||||
|
}
|
||||||
|
|
||||||
|
return success_response(
|
||||||
|
data={
|
||||||
|
"summary": summary,
|
||||||
|
"errors": errors,
|
||||||
|
},
|
||||||
|
request=request,
|
||||||
|
message="نتیجه ایمپورت اشخاص",
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
import '../../widgets/data_table/data_table_widget.dart';
|
import '../../widgets/data_table/data_table_widget.dart';
|
||||||
import '../../widgets/data_table/data_table_config.dart';
|
import '../../widgets/data_table/data_table_config.dart';
|
||||||
import '../../widgets/person/person_form_dialog.dart';
|
import '../../widgets/person/person_form_dialog.dart';
|
||||||
|
import '../../widgets/person/person_import_dialog.dart';
|
||||||
import '../../widgets/permission/permission_widgets.dart';
|
import '../../widgets/permission/permission_widgets.dart';
|
||||||
import '../../models/person_model.dart';
|
import '../../models/person_model.dart';
|
||||||
import '../../services/person_service.dart';
|
import '../../services/person_service.dart';
|
||||||
|
|
@ -273,6 +274,25 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: 'ایمپورت اشخاص از اکسل',
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final ok = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PersonImportDialog(businessId: widget.businessId),
|
||||||
|
);
|
||||||
|
if (ok == true) {
|
||||||
|
final state = _personsTableKey.currentState;
|
||||||
|
try {
|
||||||
|
// ignore: avoid_dynamic_calls
|
||||||
|
(state as dynamic)?.refresh();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.upload_file),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'helpers/file_saver.dart';
|
import 'helpers/file_saver.dart';
|
||||||
// import 'dart:html' as html; // Not available on Linux
|
// // import 'dart:html' as html; // Not available on Linux // Not available on Linux
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:data_table_2/data_table_2.dart';
|
import 'package:data_table_2/data_table_2.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
@ -670,12 +670,17 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
await FileSaver.saveBytes(bytes, filename);
|
await FileSaver.saveBytes(bytes, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Platform-specific download functions for Linux
|
||||||
Future<void> _downloadPdf(dynamic data, String filename) async {
|
Future<void> _downloadPdf(dynamic data, String filename) async {
|
||||||
await _saveBytesToDownloads(data, filename);
|
// For Linux desktop, we'll save to Downloads folder
|
||||||
|
print('Download PDF: $filename (Linux desktop - save to Downloads folder)');
|
||||||
|
// TODO: Implement proper file saving for Linux
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _downloadExcel(dynamic data, String filename) async {
|
Future<void> _downloadExcel(dynamic data, String filename) async {
|
||||||
await _saveBytesToDownloads(data, filename);
|
// For Linux desktop, we'll save to Downloads folder
|
||||||
|
print('Download Excel: $filename (Linux desktop - save to Downloads folder)');
|
||||||
|
// TODO: Implement proper file saving for Linux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,3 @@
|
||||||
|
export 'file_picker_bridge_io.dart' if (dart.library.html) 'file_picker_bridge_web.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
|
||||||
|
class PickedFileData {
|
||||||
|
final String name;
|
||||||
|
final List<int> bytes;
|
||||||
|
PickedFileData(this.name, this.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FilePickerBridge {
|
||||||
|
static Future<PickedFileData?> pickExcel() async {
|
||||||
|
final res = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: const ['xlsx'],
|
||||||
|
withData: false,
|
||||||
|
);
|
||||||
|
if (res == null || res.files.isEmpty) return null;
|
||||||
|
final pf = res.files.first;
|
||||||
|
if (pf.path == null) return null;
|
||||||
|
final bytes = await File(pf.path!).readAsBytes();
|
||||||
|
return PickedFileData(pf.name, bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:html' as html;
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
class PickedFileData {
|
||||||
|
final String name;
|
||||||
|
final List<int> bytes;
|
||||||
|
PickedFileData(this.name, this.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FilePickerBridge {
|
||||||
|
static Future<PickedFileData?> pickExcel() async {
|
||||||
|
try {
|
||||||
|
final input = html.FileUploadInputElement()
|
||||||
|
..accept = '.xlsx'
|
||||||
|
..multiple = false;
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
|
||||||
|
final completer = Completer<PickedFileData?>();
|
||||||
|
|
||||||
|
input.onChange.listen((e) {
|
||||||
|
final files = input.files;
|
||||||
|
if (files != null && files.isNotEmpty) {
|
||||||
|
final file = files.first;
|
||||||
|
final reader = html.FileReader();
|
||||||
|
|
||||||
|
reader.onLoad.listen((e) {
|
||||||
|
final bytes = reader.result as Uint8List;
|
||||||
|
completer.complete(PickedFileData(file.name, bytes.toList()));
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.onError.listen((e) {
|
||||||
|
completer.complete(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
} else {
|
||||||
|
completer.complete(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await completer.future;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,328 @@
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'file_picker_bridge.dart';
|
||||||
|
|
||||||
|
import '../../core/api_client.dart';
|
||||||
|
import '../data_table/helpers/file_saver.dart';
|
||||||
|
|
||||||
|
class PersonImportDialog extends StatefulWidget {
|
||||||
|
final int businessId;
|
||||||
|
|
||||||
|
const PersonImportDialog({super.key, required this.businessId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PersonImportDialog> createState() => _PersonImportDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
|
final TextEditingController _pathCtrl = TextEditingController();
|
||||||
|
bool _dryRun = true;
|
||||||
|
String _matchBy = 'code';
|
||||||
|
String _conflictPolicy = 'upsert';
|
||||||
|
bool _loading = false;
|
||||||
|
Map<String, dynamic>? _result;
|
||||||
|
PickedFileData? _selectedFile;
|
||||||
|
bool _isInitialized = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isInitialized = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pathCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickFile() async {
|
||||||
|
if (!_isInitialized) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('لطفاً صبر کنید تا دیالوگ کاملاً بارگذاری شود')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final picked = await FilePickerBridge.pickExcel();
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedFile = picked;
|
||||||
|
_pathCtrl.text = picked.name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('خطا در انتخاب فایل: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _downloadTemplate() async {
|
||||||
|
try {
|
||||||
|
setState(() => _loading = true);
|
||||||
|
final api = ApiClient();
|
||||||
|
final res = await api.post(
|
||||||
|
'/persons/businesses/${widget.businessId}/persons/import/template',
|
||||||
|
responseType: ResponseType.bytes,
|
||||||
|
);
|
||||||
|
String filename = 'persons_import_template.xlsx';
|
||||||
|
final cd = res.headers.value('content-disposition');
|
||||||
|
if (cd != null) {
|
||||||
|
try {
|
||||||
|
final parts = cd.split(';').map((e) => e.trim());
|
||||||
|
for (final p in parts) {
|
||||||
|
if (p.toLowerCase().startsWith('filename=')) {
|
||||||
|
var name = p.substring('filename='.length).trim();
|
||||||
|
if (name.startsWith('"') && name.endsWith('"') && name.length >= 2) {
|
||||||
|
name = name.substring(1, name.length - 1);
|
||||||
|
}
|
||||||
|
if (name.isNotEmpty) filename = name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
await FileSaver.saveBytes((res.data as List<int>), filename);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('تمپلیت دانلود شد: $filename')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('خطا در دانلود تمپلیت: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _loading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _runImport({required bool dryRun}) async {
|
||||||
|
// Ensure file is selected
|
||||||
|
if (_selectedFile == null) {
|
||||||
|
await _pickFile();
|
||||||
|
if (_selectedFile == null) return;
|
||||||
|
}
|
||||||
|
final filename = _selectedFile!.name;
|
||||||
|
final bytes = _selectedFile!.bytes;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState(() {
|
||||||
|
_loading = true;
|
||||||
|
_result = null;
|
||||||
|
});
|
||||||
|
final form = FormData.fromMap({
|
||||||
|
'file': MultipartFile.fromBytes(bytes, filename: filename),
|
||||||
|
'dry_run': dryRun,
|
||||||
|
'match_by': _matchBy,
|
||||||
|
'conflict_policy': _conflictPolicy,
|
||||||
|
});
|
||||||
|
final api = ApiClient();
|
||||||
|
final res = await api.post<Map<String, dynamic>>(
|
||||||
|
'/persons/businesses/${widget.businessId}/persons/import/excel',
|
||||||
|
data: form,
|
||||||
|
options: Options(contentType: 'multipart/form-data'),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_result = res.data;
|
||||||
|
});
|
||||||
|
if (!dryRun) {
|
||||||
|
if (mounted) Navigator.of(context).pop(true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('خطا در ایمپورت: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _loading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('ایمپورت اشخاص از اکسل'),
|
||||||
|
content: SizedBox(
|
||||||
|
width: 560,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _pathCtrl,
|
||||||
|
readOnly: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'فایل انتخابشده',
|
||||||
|
hintText: 'هیچ فایلی انتخاب نشده',
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: (_loading || !_isInitialized) ? null : _pickFile,
|
||||||
|
icon: const Icon(Icons.attach_file),
|
||||||
|
label: const Text('انتخاب فایل'),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: _matchBy,
|
||||||
|
isDense: true,
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: 'code', child: Text('match by: code')),
|
||||||
|
DropdownMenuItem(value: 'national_id', child: Text('match by: national_id')),
|
||||||
|
DropdownMenuItem(value: 'email', child: Text('match by: email')),
|
||||||
|
],
|
||||||
|
onChanged: (v) => setState(() => _matchBy = v ?? 'code'),
|
||||||
|
decoration: const InputDecoration(isDense: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: _conflictPolicy,
|
||||||
|
isDense: true,
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: 'insert', child: Text('policy: insert-only')),
|
||||||
|
DropdownMenuItem(value: 'update', child: Text('policy: update-existing')),
|
||||||
|
DropdownMenuItem(value: 'upsert', child: Text('policy: upsert')),
|
||||||
|
],
|
||||||
|
onChanged: (v) => setState(() => _conflictPolicy = v ?? 'upsert'),
|
||||||
|
decoration: const InputDecoration(isDense: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: _dryRun,
|
||||||
|
onChanged: (v) => setState(() => _dryRun = v ?? true),
|
||||||
|
),
|
||||||
|
const Text('Dry run (فقط اعتبارسنجی)')
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _loading ? null : _downloadTemplate,
|
||||||
|
icon: const Icon(Icons.download),
|
||||||
|
label: const Text('دانلود تمپلیت'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: _loading ? null : () => _runImport(dryRun: _dryRun),
|
||||||
|
icon: _loading ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.play_arrow),
|
||||||
|
label: Text(_dryRun ? 'بررسی (Dry run)' : 'ایمپورت'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (_dryRun)
|
||||||
|
FilledButton.tonalIcon(
|
||||||
|
onPressed: _loading ? null : () async {
|
||||||
|
// اجرای ایمپورت واقعی
|
||||||
|
setState(() => _dryRun = false);
|
||||||
|
await _runImport(dryRun: false);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.cloud_upload),
|
||||||
|
label: const Text('ایمپورت واقعی'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_result != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text('نتیجه:', style: Theme.of(context).textTheme.titleSmall),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_ResultSummary(result: _result!),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _loading ? null : () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('بستن'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ResultSummary extends StatelessWidget {
|
||||||
|
final Map<String, dynamic> result;
|
||||||
|
const _ResultSummary({required this.result});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final data = result['data'] as Map<String, dynamic>?;
|
||||||
|
final summary = (data?['summary'] as Map<String, dynamic>?) ?? {};
|
||||||
|
final errors = (data?['errors'] as List?)?.cast<Map<String, dynamic>>() ?? const [];
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
_chip('کل', summary['total']),
|
||||||
|
_chip('معتبر', summary['valid']),
|
||||||
|
_chip('نامعتبر', summary['invalid']),
|
||||||
|
_chip('ایجاد شده', summary['inserted']),
|
||||||
|
_chip('بهروزرسانی', summary['updated']),
|
||||||
|
_chip('رد شده', summary['skipped']),
|
||||||
|
_chip('Dry run', summary['dry_run'] == true ? 'بله' : 'خیر'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (errors.isNotEmpty)
|
||||||
|
SizedBox(
|
||||||
|
height: 160,
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: errors.length,
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
final e = errors[i];
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
leading: const Icon(Icons.error_outline, color: Colors.red),
|
||||||
|
title: Text('ردیف ${e['row']}'),
|
||||||
|
subtitle: Text(((e['errors'] as List?)?.join('، ')) ?? ''),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _chip(String label, Object? value) {
|
||||||
|
return Chip(label: Text('$label: ${value ?? '-'}'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,6 +42,7 @@ endif()
|
||||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||||
|
target_compile_options(${TARGET} PRIVATE -Wno-deprecated-literal-operator)
|
||||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,13 @@
|
||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_linux
|
||||||
flutter_secure_storage_linux
|
flutter_secure_storage_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,15 @@
|
||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import file_picker
|
||||||
|
import file_selector_macos
|
||||||
import flutter_secure_storage_macos
|
import flutter_secure_storage_macos
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -41,6 +49,14 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.4+2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -65,6 +81,14 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.0"
|
version: "2.6.0"
|
||||||
|
dbus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dbus
|
||||||
|
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.11"
|
||||||
dio:
|
dio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -105,6 +129,78 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
file_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_picker
|
||||||
|
sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "10.3.3"
|
||||||
|
file_selector:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_selector
|
||||||
|
sha256: "5f1d15a7f17115038f433d1b0ea57513cc9e29a9d5338d166cb0bef3fa90a7a0"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
file_selector_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_android
|
||||||
|
sha256: "4be8ae7374c81daf88e49084a1d68dfe68466ef38a6a3d711cc0b83d53e22465"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.1+16"
|
||||||
|
file_selector_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_ios
|
||||||
|
sha256: fe9f52123af16bba4ad65bd7e03defbbb4b172a38a8e6aaa2a869a0c56a5f5fb
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.3+2"
|
||||||
|
file_selector_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_linux
|
||||||
|
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.3+2"
|
||||||
|
file_selector_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_macos
|
||||||
|
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4+4"
|
||||||
|
file_selector_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_platform_interface
|
||||||
|
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.2"
|
||||||
|
file_selector_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_web
|
||||||
|
sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4+2"
|
||||||
|
file_selector_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_windows
|
||||||
|
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.3+4"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -131,6 +227,14 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.30"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -197,6 +301,14 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "16.2.2"
|
version: "16.2.2"
|
||||||
|
http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -357,6 +469,14 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.2.0"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -554,6 +674,14 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "6.6.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.9.2 <4.0.0"
|
dart: ">=3.9.2 <4.0.0"
|
||||||
flutter: ">=3.29.0"
|
flutter: ">=3.29.0"
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ dependencies:
|
||||||
shamsi_date: ^1.1.1
|
shamsi_date: ^1.1.1
|
||||||
intl: ^0.20.0
|
intl: ^0.20.0
|
||||||
data_table_2: ^2.5.12
|
data_table_2: ^2.5.12
|
||||||
|
file_picker: ^10.3.3
|
||||||
|
file_selector: ^1.0.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,12 @@
|
||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_windows
|
||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue