Files
Emre Emir 8bbc9dbff2 Initial commit: DLS - Dental Lab System
- Flutter + PocketBase dental lab management system
- Clinic & lab dashboards, job tracking, patient management
- Product catalog, finance tracking, multi-language support
- AI assistant integration, realtime notifications
- Windows installer (Inno Setup) included
- Developed by kovakyazilim.com
2026-06-11 15:57:31 +03:00

172 lines
5.9 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import '../../features/shared/job_files_repository.dart';
import '../../features/shared/tenant_team_repository.dart';
import '../../models/job_file.dart';
import '../../models/tenant.dart';
import '../api/pocketbase_client.dart';
// ── Message segments ──────────────────────────────────────────────────────────
sealed class MessageSegment {}
class TextSegment extends MessageSegment {
TextSegment(this.text);
final String text;
}
class ActionSegment extends MessageSegment {
ActionSegment(this.action);
final AiAction action;
}
// ── Action model ──────────────────────────────────────────────────────────────
class AiAction {
const AiAction({
required this.type,
required this.params,
required this.label,
});
final String type;
final Map<String, String> params;
final String label;
bool get isDangerous => type == 'cancel_job';
bool get isFileAction => type == 'job_files';
}
// ── Action outcome ────────────────────────────────────────────────────────────
sealed class ActionOutcome {}
class ActionSuccess extends ActionOutcome {
ActionSuccess(this.message);
final String message;
}
class ActionError extends ActionOutcome {
ActionError(this.error);
final String error;
}
class ActionFiles extends ActionOutcome {
ActionFiles(this.files);
final List<JobFile> files;
}
// ── Parser ────────────────────────────────────────────────────────────────────
List<MessageSegment> parseSegments(String text) {
// Strip code fences wrapping <dls-action> tags that the AI sometimes emits.
// Handles: ```xml\n<dls-action .../>\n``` and ```\n<dls-action .../>\n```
text = text.replaceAllMapped(
RegExp(r'```(?:xml)?\s*\n(\s*<dls-action\s[^>]*/>)\s*\n\s*```'),
(m) => m.group(1)!,
);
// Also handle inline variant: ```xml <dls-action .../> ```
text = text.replaceAllMapped(
RegExp(r'```(?:xml)?\s*(<dls-action\s[^>]*/>)\s*```'),
(m) => m.group(1)!,
);
final pattern = RegExp(r'<dls-action\s([^/]*?)/>', dotAll: true);
final segments = <MessageSegment>[];
int last = 0;
for (final m in pattern.allMatches(text)) {
final before = text.substring(last, m.start).trim();
if (before.isNotEmpty) segments.add(TextSegment(before));
final attrs = _parseAttrs(m.group(1) ?? '');
segments.add(ActionSegment(AiAction(
type: attrs['type'] ?? '',
params: attrs,
label: attrs['label'] ?? attrs['type'] ?? 'İşlem',
)));
last = m.end;
}
final rest = text.substring(last).trim();
if (rest.isNotEmpty) segments.add(TextSegment(rest));
return segments;
}
Map<String, String> _parseAttrs(String s) {
final result = <String, String>{};
for (final m in RegExp(r'(\w+)="([^"]*)"').allMatches(s)) {
result[m.group(1)!] = m.group(2)!;
}
return result;
}
// ── Executor ──────────────────────────────────────────────────────────────────
class AiActionExecutor {
static final _pb = PocketBaseClient.instance.pb;
static Future<ActionOutcome> execute(
AiAction action,
TenantMembership membership,
) async {
try {
return switch (action.type) {
'cancel_job' => await _cancelJob(action.params),
'mark_delivered' => await _markDelivered(action.params),
'job_files' => await _jobFiles(action.params),
'add_member' => await _addMember(action.params, membership),
_ => ActionError('Bilinmeyen işlem türü: ${action.type}'),
};
} catch (e) {
final msg = e.toString();
if (msg.length > 120) return ActionError('Sunucu hatası');
return ActionError(msg);
}
}
static Future<ActionOutcome> _cancelJob(Map<String, String> p) async {
final id = p['job_id'];
if (id == null || id.isEmpty) return ActionError('İş ID bulunamadı.');
await _pb.collection('jobs').update(id, body: {'status': 'cancelled'});
return ActionSuccess('İş başarıyla iptal edildi.');
}
static Future<ActionOutcome> _markDelivered(Map<String, String> p) async {
final id = p['job_id'];
if (id == null || id.isEmpty) return ActionError('İş ID bulunamadı.');
await _pb.collection('jobs').update(id, body: {'status': 'delivered'});
return ActionSuccess('İş teslim edildi olarak işaretlendi.');
}
static Future<ActionOutcome> _jobFiles(Map<String, String> p) async {
final id = p['job_id'];
if (id == null || id.isEmpty) return ActionError('İş ID bulunamadı.');
final files = await JobFilesRepository.instance.listForJob(id);
if (files.isEmpty) return ActionSuccess('Bu iş için henüz dosya yüklenmemiş.');
return ActionFiles(files);
}
static Future<ActionOutcome> _addMember(
Map<String, String> p,
TenantMembership membership,
) async {
final email = p['email'];
final firstName = p['first_name'];
final lastName = p['last_name'] ?? '';
final role = p['role'];
final password = p['password'];
if (email == null || firstName == null || role == null || password == null) {
return ActionError('Eksik bilgi: e-posta, ad, rol ve şifre gerekli.');
}
await TenantTeamRepository.instance.addMember(
tenantId: membership.tenant.id,
email: email,
password: password,
firstName: firstName,
lastName: lastName,
role: TenantMembership.parseRole(role),
);
return ActionSuccess('$firstName $lastName ekibe eklendi.');
}
}