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
This commit is contained in:
Emre Emir
2026-06-11 15:57:31 +03:00
commit 8bbc9dbff2
226 changed files with 31308 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
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.');
}
}