diff --git a/composer.json b/composer.json
index 915e946..919c6de 100644
--- a/composer.json
+++ b/composer.json
@@ -21,7 +21,8 @@
"guzzlehttp/guzzle": "^7.9",
"respect/validation": "^2.3",
"league/flysystem": "^3.28",
- "symfony/mailer": "^7.1"
+ "symfony/mailer": "^7.1",
+ "phpoffice/phpspreadsheet": "^2.1"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
diff --git a/musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/AccentColor.colorset/Contents.json b/musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/AppIcon.appiconset/Contents.json b/musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..2305880
--- /dev/null
+++ b/musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,35 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/Contents.json b/musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/WidgetBackground.colorset/Contents.json b/musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/WidgetBackground.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/WidgetBackground.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/musadaq-app/ios/MusadaqLiveActivity/Info.plist b/musadaq-app/ios/MusadaqLiveActivity/Info.plist
new file mode 100644
index 0000000..0f118fb
--- /dev/null
+++ b/musadaq-app/ios/MusadaqLiveActivity/Info.plist
@@ -0,0 +1,11 @@
+
+
+
+
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.widgetkit-extension
+
+
+
diff --git a/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivity.swift b/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivity.swift
new file mode 100644
index 0000000..8283e19
--- /dev/null
+++ b/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivity.swift
@@ -0,0 +1,84 @@
+//
+// MusadaqLiveActivity.swift
+// MusadaqLiveActivity
+//
+// Created by Hamza Aleghwairyeen on 07/05/2026.
+//
+
+import WidgetKit
+import SwiftUI
+
+struct Provider: TimelineProvider {
+ func placeholder(in context: Context) -> SimpleEntry {
+ SimpleEntry(date: Date(), emoji: "😀")
+ }
+
+ func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
+ let entry = SimpleEntry(date: Date(), emoji: "😀")
+ completion(entry)
+ }
+
+ func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) {
+ var entries: [SimpleEntry] = []
+
+ // Generate a timeline consisting of five entries an hour apart, starting from the current date.
+ let currentDate = Date()
+ for hourOffset in 0 ..< 5 {
+ let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
+ let entry = SimpleEntry(date: entryDate, emoji: "😀")
+ entries.append(entry)
+ }
+
+ let timeline = Timeline(entries: entries, policy: .atEnd)
+ completion(timeline)
+ }
+
+// func relevances() async -> WidgetRelevances {
+// // Generate a list containing the contexts this widget is relevant in.
+// }
+}
+
+struct SimpleEntry: TimelineEntry {
+ let date: Date
+ let emoji: String
+}
+
+struct MusadaqLiveActivityEntryView : View {
+ var entry: Provider.Entry
+
+ var body: some View {
+ VStack {
+ Text("Time:")
+ Text(entry.date, style: .time)
+
+ Text("Emoji:")
+ Text(entry.emoji)
+ }
+ }
+}
+
+struct MusadaqLiveActivity: Widget {
+ let kind: String = "MusadaqLiveActivity"
+
+ var body: some WidgetConfiguration {
+ StaticConfiguration(kind: kind, provider: Provider()) { entry in
+ if #available(iOS 17.0, *) {
+ MusadaqLiveActivityEntryView(entry: entry)
+ .containerBackground(.fill.tertiary, for: .widget)
+ } else {
+ MusadaqLiveActivityEntryView(entry: entry)
+ .padding()
+ .background()
+ }
+ }
+ .configurationDisplayName("My Widget")
+ .description("This is an example widget.")
+ }
+}
+
+#Preview(as: .systemSmall) {
+ MusadaqLiveActivity()
+} timeline: {
+ SimpleEntry(date: .now, emoji: "😀")
+ SimpleEntry(date: .now, emoji: "🤩")
+}
diff --git a/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityBundle.swift b/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityBundle.swift
new file mode 100644
index 0000000..18c4a09
--- /dev/null
+++ b/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityBundle.swift
@@ -0,0 +1,86 @@
+//
+// MusadaqLiveActivityBundle.swift
+// MusadaqLiveActivity
+//
+// Created by Hamza Aleghwairyeen on 07/05/2026.
+//
+
+import WidgetKit
+import SwiftUI
+import ActivityKit
+
+// ─── 1. Data Model ───────────────────────────────────
+struct InvoiceBatchAttributes: ActivityAttributes {
+ public struct ContentState: Codable, Hashable {
+ var current: Int
+ var total: Int
+ var isDone: Bool
+ }
+ var companyName: String
+}
+
+// ─── 2. Bundle ───────────────────────────────────────
+@main
+struct MusadaqLiveActivityBundle: WidgetBundle {
+ var body: some Widget {
+ MusadaqLiveActivityLiveActivity()
+ }
+}
+
+// ─── 3. Widget ───────────────────────────────────────
+struct MusadaqLiveActivityLiveActivity: Widget {
+ var body: some WidgetConfiguration {
+ ActivityConfiguration(for: InvoiceBatchAttributes.self) { context in
+ // Lock Screen UI
+ ZStack {
+ Color(red: 0.043, green: 0.098, blue: 0.161) // #0B1929
+ HStack(spacing: 12) {
+ Image(systemName: context.state.isDone
+ ? "checkmark.doc.fill" : "arrow.up.doc.fill")
+ .foregroundColor(Color(red: 0.831, green: 0.659, blue: 0.263)) // #D4A843
+ .font(.title2)
+ VStack(alignment: .leading, spacing: 4) {
+ Text(context.state.isDone ? "✅ تم الرفع بنجاح" : "مُصادَق — جارٍ الرفع...")
+ .font(.caption.bold())
+ .foregroundColor(.white)
+ ProgressView(
+ value: Double(context.state.current),
+ total: Double(context.state.total)
+ )
+ .tint(Color(red: 0.831, green: 0.659, blue: 0.263))
+ Text("\(context.state.current) / \(context.state.total) فاتورة — \(context.attributes.companyName)")
+ .font(.caption2)
+ .foregroundColor(.gray)
+ }
+ }
+ .padding()
+ }
+ } dynamicIsland: { context in
+ DynamicIsland {
+ DynamicIslandExpandedRegion(.leading) {
+ Image(systemName: "arrow.up.doc.fill")
+ .foregroundColor(Color(red: 0.831, green: 0.659, blue: 0.263))
+ }
+ DynamicIslandExpandedRegion(.trailing) {
+ Text("\(context.state.current)/\(context.state.total)")
+ .font(.caption.bold()).foregroundColor(.white)
+ }
+ DynamicIslandExpandedRegion(.bottom) {
+ ProgressView(value: Double(context.state.current),
+ total: Double(context.state.total))
+ .tint(Color(red: 0.831, green: 0.659, blue: 0.263))
+ }
+ } compactLeading: {
+ Image(systemName: "arrow.up.doc.fill")
+ .foregroundColor(Color(red: 0.831, green: 0.659, blue: 0.263))
+ .font(.caption)
+ } compactTrailing: {
+ Text("\(context.state.current)/\(context.state.total)")
+ .font(.caption2.bold()).foregroundColor(.white)
+ } minimal: {
+ Image(systemName: "arrow.up.doc.fill")
+ .foregroundColor(Color(red: 0.831, green: 0.659, blue: 0.263))
+ }
+ }
+ }
+}
diff --git a/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityControl.swift b/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityControl.swift
new file mode 100644
index 0000000..72653e1
--- /dev/null
+++ b/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityControl.swift
@@ -0,0 +1,54 @@
+//
+// MusadaqLiveActivityControl.swift
+// MusadaqLiveActivity
+//
+// Created by Hamza Aleghwairyeen on 07/05/2026.
+//
+
+import AppIntents
+import SwiftUI
+import WidgetKit
+
+struct MusadaqLiveActivityControl: ControlWidget {
+ var body: some ControlWidgetConfiguration {
+ StaticControlConfiguration(
+ kind: "com.example.musadaqApp.MusadaqLiveActivity",
+ provider: Provider()
+ ) { value in
+ ControlWidgetToggle(
+ "Start Timer",
+ isOn: value,
+ action: StartTimerIntent()
+ ) { isRunning in
+ Label(isRunning ? "On" : "Off", systemImage: "timer")
+ }
+ }
+ .displayName("Timer")
+ .description("A an example control that runs a timer.")
+ }
+}
+
+extension MusadaqLiveActivityControl {
+ struct Provider: ControlValueProvider {
+ var previewValue: Bool {
+ false
+ }
+
+ func currentValue() async throws -> Bool {
+ let isRunning = true // Check if the timer is running
+ return isRunning
+ }
+ }
+}
+
+struct StartTimerIntent: SetValueIntent {
+ static let title: LocalizedStringResource = "Start a timer"
+
+ @Parameter(title: "Timer is running")
+ var value: Bool
+
+ func perform() async throws -> some IntentResult {
+ // Start / stop the timer based on `value`.
+ return .result()
+ }
+}
diff --git a/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityLiveActivity.swift b/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityLiveActivity.swift
new file mode 100644
index 0000000..61933cd
--- /dev/null
+++ b/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityLiveActivity.swift
@@ -0,0 +1,80 @@
+//
+// MusadaqLiveActivityLiveActivity.swift
+// MusadaqLiveActivity
+//
+// Created by Hamza Aleghwairyeen on 07/05/2026.
+//
+
+import ActivityKit
+import WidgetKit
+import SwiftUI
+
+struct MusadaqLiveActivityAttributes: ActivityAttributes {
+ public struct ContentState: Codable, Hashable {
+ // Dynamic stateful properties about your activity go here!
+ var emoji: String
+ }
+
+ // Fixed non-changing properties about your activity go here!
+ var name: String
+}
+
+struct MusadaqLiveActivityLiveActivity: Widget {
+ var body: some WidgetConfiguration {
+ ActivityConfiguration(for: MusadaqLiveActivityAttributes.self) { context in
+ // Lock screen/banner UI goes here
+ VStack {
+ Text("Hello \(context.state.emoji)")
+ }
+ .activityBackgroundTint(Color.cyan)
+ .activitySystemActionForegroundColor(Color.black)
+
+ } dynamicIsland: { context in
+ DynamicIsland {
+ // Expanded UI goes here. Compose the expanded UI through
+ // various regions, like leading/trailing/center/bottom
+ DynamicIslandExpandedRegion(.leading) {
+ Text("Leading")
+ }
+ DynamicIslandExpandedRegion(.trailing) {
+ Text("Trailing")
+ }
+ DynamicIslandExpandedRegion(.bottom) {
+ Text("Bottom \(context.state.emoji)")
+ // more content
+ }
+ } compactLeading: {
+ Text("L")
+ } compactTrailing: {
+ Text("T \(context.state.emoji)")
+ } minimal: {
+ Text(context.state.emoji)
+ }
+ .widgetURL(URL(string: "http://www.apple.com"))
+ .keylineTint(Color.red)
+ }
+ }
+}
+
+extension MusadaqLiveActivityAttributes {
+ fileprivate static var preview: MusadaqLiveActivityAttributes {
+ MusadaqLiveActivityAttributes(name: "World")
+ }
+}
+
+extension MusadaqLiveActivityAttributes.ContentState {
+ fileprivate static var smiley: MusadaqLiveActivityAttributes.ContentState {
+ MusadaqLiveActivityAttributes.ContentState(emoji: "😀")
+ }
+
+ fileprivate static var starEyes: MusadaqLiveActivityAttributes.ContentState {
+ MusadaqLiveActivityAttributes.ContentState(emoji: "🤩")
+ }
+}
+
+#Preview("Notification", as: .content, using: MusadaqLiveActivityAttributes.preview) {
+ MusadaqLiveActivityLiveActivity()
+} contentStates: {
+ MusadaqLiveActivityAttributes.ContentState.smiley
+ MusadaqLiveActivityAttributes.ContentState.starEyes
+}
diff --git a/musadaq-app/ios/MusadaqLiveActivityExtension.entitlements b/musadaq-app/ios/MusadaqLiveActivityExtension.entitlements
new file mode 100644
index 0000000..5aaef89
--- /dev/null
+++ b/musadaq-app/ios/MusadaqLiveActivityExtension.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.com.musadaq.app
+
+
+
diff --git a/musadaq-app/ios/Runner.xcodeproj/project.pbxproj b/musadaq-app/ios/Runner.xcodeproj/project.pbxproj
index 587ab53..7a136c7 100644
--- a/musadaq-app/ios/Runner.xcodeproj/project.pbxproj
+++ b/musadaq-app/ios/Runner.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 54;
+ objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
@@ -17,6 +17,9 @@
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
BF0A71587203972CC86D9A9D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D6221B80BFD02DE32675609 /* Pods_Runner.framework */; };
+ C68ADD2A2FACB4B8000DB48F /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C68ADD292FACB4B8000DB48F /* WidgetKit.framework */; };
+ C68ADD2C2FACB4B8000DB48F /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C68ADD2B2FACB4B8000DB48F /* SwiftUI.framework */; };
+ C68ADD3B2FACB4BA000DB48F /* MusadaqLiveActivityExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C68ADD282FACB4B8000DB48F /* MusadaqLiveActivityExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -27,6 +30,13 @@
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
+ C68ADD392FACB4BA000DB48F /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = C68ADD272FACB4B8000DB48F;
+ remoteInfo = MusadaqLiveActivityExtension;
+ };
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -40,6 +50,17 @@
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
+ C68ADD3C2FACB4BA000DB48F /* Embed Foundation Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ C68ADD3B2FACB4BA000DB48F /* MusadaqLiveActivityExtension.appex in Embed Foundation Extensions */,
+ );
+ name = "Embed Foundation Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
@@ -67,8 +88,27 @@
A516B9E15A13BFDC50FA3878 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
B5F124508D0E246590A34406 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C1D35ECD47C3CDCBD2C89CB0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ C68ADD282FACB4B8000DB48F /* MusadaqLiveActivityExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MusadaqLiveActivityExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ C68ADD292FACB4B8000DB48F /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
+ C68ADD2B2FACB4B8000DB48F /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
+ C68ADD422FACB594000DB48F /* MusadaqLiveActivityExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MusadaqLiveActivityExtension.entitlements; sourceTree = ""; };
+ C68ADD432FACBB13000DB48F /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
/* End PBXFileReference section */
+/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
+ C68ADD402FACB4BA000DB48F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Info.plist,
+ );
+ target = C68ADD272FACB4B8000DB48F /* MusadaqLiveActivityExtension */;
+ };
+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ C68ADD2D2FACB4B8000DB48F /* MusadaqLiveActivity */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C68ADD402FACB4BA000DB48F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = MusadaqLiveActivity; sourceTree = ""; };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
/* Begin PBXFrameworksBuildPhase section */
61D890350FB4633E292F3803 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -86,6 +126,15 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ C68ADD252FACB4B8000DB48F /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ C68ADD2C2FACB4B8000DB48F /* SwiftUI.framework in Frameworks */,
+ C68ADD2A2FACB4B8000DB48F /* WidgetKit.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -107,7 +156,6 @@
4CDBE737BB43361B59042085 /* Pods-RunnerTests.release.xcconfig */,
A516B9E15A13BFDC50FA3878 /* Pods-RunnerTests.profile.xcconfig */,
);
- name = Pods;
path = Pods;
sourceTree = "";
};
@@ -125,8 +173,10 @@
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
+ C68ADD422FACB594000DB48F /* MusadaqLiveActivityExtension.entitlements */,
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
+ C68ADD2D2FACB4B8000DB48F /* MusadaqLiveActivity */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
01F4F223F169A9E6C26FA35C /* GoogleService-Info.plist */,
@@ -140,6 +190,7 @@
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
+ C68ADD282FACB4B8000DB48F /* MusadaqLiveActivityExtension.appex */,
);
name = Products;
sourceTree = "";
@@ -147,6 +198,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
+ C68ADD432FACBB13000DB48F /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -164,6 +216,8 @@
children = (
7D6221B80BFD02DE32675609 /* Pods_Runner.framework */,
B5F124508D0E246590A34406 /* Pods_RunnerTests.framework */,
+ C68ADD292FACB4B8000DB48F /* WidgetKit.framework */,
+ C68ADD2B2FACB4B8000DB48F /* SwiftUI.framework */,
);
name = Frameworks;
sourceTree = "";
@@ -200,6 +254,7 @@
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
+ C68ADD3C2FACB4BA000DB48F /* Embed Foundation Extensions */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
E56C32186B9FB7A499F5B38F /* [CP] Embed Pods Frameworks */,
B23DF4822D0EB618AED457C9 /* [CP] Copy Pods Resources */,
@@ -207,12 +262,35 @@
buildRules = (
);
dependencies = (
+ C68ADD3A2FACB4BA000DB48F /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
+ C68ADD272FACB4B8000DB48F /* MusadaqLiveActivityExtension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = C68ADD412FACB4BA000DB48F /* Build configuration list for PBXNativeTarget "MusadaqLiveActivityExtension" */;
+ buildPhases = (
+ C68ADD242FACB4B8000DB48F /* Sources */,
+ C68ADD262FACB4B8000DB48F /* Resources */,
+ C68ADD252FACB4B8000DB48F /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ fileSystemSynchronizedGroups = (
+ C68ADD2D2FACB4B8000DB48F /* MusadaqLiveActivity */,
+ );
+ name = MusadaqLiveActivityExtension;
+ packageProductDependencies = (
+ );
+ productName = MusadaqLiveActivityExtension;
+ productReference = C68ADD282FACB4B8000DB48F /* MusadaqLiveActivityExtension.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -220,6 +298,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
+ LastSwiftUpdateCheck = 2600;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@@ -231,6 +310,9 @@
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
+ C68ADD272FACB4B8000DB48F = {
+ CreatedOnToolsVersion = 26.0;
+ };
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
@@ -248,6 +330,7 @@
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
+ C68ADD272FACB4B8000DB48F /* MusadaqLiveActivityExtension */,
);
};
/* End PBXProject section */
@@ -272,6 +355,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ C68ADD262FACB4B8000DB48F /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -336,10 +426,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
+ inputPaths = (
+ );
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
+ outputPaths = (
+ );
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -375,10 +469,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
+ inputPaths = (
+ );
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
+ outputPaths = (
+ );
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -404,6 +502,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ C68ADD242FACB4B8000DB48F /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@@ -412,6 +517,11 @@
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
+ C68ADD3A2FACB4BA000DB48F /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = C68ADD272FACB4B8000DB48F /* MusadaqLiveActivityExtension */;
+ targetProxy = C68ADD392FACB4BA000DB48F /* PBXContainerItemProxy */;
+ };
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -492,6 +602,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
@@ -500,7 +611,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.example.musadaqApp;
+ PRODUCT_BUNDLE_IDENTIFIER = com.musadaq.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -675,6 +786,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
@@ -683,7 +795,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.example.musadaqApp;
+ PRODUCT_BUNDLE_IDENTIFIER = com.musadaq.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -698,6 +810,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
@@ -706,7 +819,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.example.musadaqApp;
+ PRODUCT_BUNDLE_IDENTIFIER = com.musadaq.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -714,6 +827,138 @@
};
name = Release;
};
+ C68ADD3D2FACB4BA000DB48F /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = MusadaqLiveActivityExtension.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 63CVT8G5P8;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = MusadaqLiveActivity/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = MusadaqLiveActivity;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.musadaq.app.MusadaqLiveActivity;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ C68ADD3E2FACB4BA000DB48F /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = MusadaqLiveActivityExtension.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 63CVT8G5P8;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = MusadaqLiveActivity/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = MusadaqLiveActivity;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.musadaq.app.MusadaqLiveActivity;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ C68ADD3F2FACB4BA000DB48F /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = MusadaqLiveActivityExtension.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 63CVT8G5P8;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = MusadaqLiveActivity/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = MusadaqLiveActivity;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.musadaq.app.MusadaqLiveActivity;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Profile;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -747,6 +992,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ C68ADD412FACB4BA000DB48F /* Build configuration list for PBXNativeTarget "MusadaqLiveActivityExtension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ C68ADD3D2FACB4BA000DB48F /* Debug */,
+ C68ADD3E2FACB4BA000DB48F /* Release */,
+ C68ADD3F2FACB4BA000DB48F /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
diff --git a/musadaq-app/ios/Runner/Info.plist b/musadaq-app/ios/Runner/Info.plist
index 2d22289..b1e5f9e 100644
--- a/musadaq-app/ios/Runner/Info.plist
+++ b/musadaq-app/ios/Runner/Info.plist
@@ -34,6 +34,8 @@
تطبيق مُصادَق يحتاج للوصول إلى الميكروفون لاستخدام المساعد الصوتي وتسجيل الملاحظات.
NSPhotoLibraryUsageDescription
تطبيق مُصادَق يحتاج للوصول إلى الصور لاختيار فواتير محفوظة مسبقاً في معرض الصور.
+ NSSupportsLiveActivities
+
UIApplicationSceneManifest
UIApplicationSupportsMultipleScenes
@@ -74,5 +76,9 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ NSSupportsLiveActivities
+
+ NSSupportsLiveActivitiesFrequentUpdates
+
diff --git a/musadaq-app/ios/Runner/Runner.entitlements b/musadaq-app/ios/Runner/Runner.entitlements
new file mode 100644
index 0000000..5aaef89
--- /dev/null
+++ b/musadaq-app/ios/Runner/Runner.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.com.musadaq.app
+
+
+
diff --git a/scratch/run_migration.php b/scratch/run_migration.php
new file mode 100644
index 0000000..f8e2ae2
--- /dev/null
+++ b/scratch/run_migration.php
@@ -0,0 +1,22 @@
+getConnection();
+
+try {
+ // MySQL PDO cannot run multiple statements with exec() sometimes depending on driver,
+ // so we split by semicolon and handle.
+ $statements = array_filter(array_map('trim', explode(';', $sql)));
+
+ foreach ($statements as $stmt) {
+ if (!empty($stmt)) {
+ $db->exec($stmt);
+ echo "Executed: " . substr($stmt, 0, 50) . "...\n";
+ }
+ }
+ echo "Migration completed successfully!\n";
+} catch (Exception $e) {
+ echo "Migration failed: " . $e->getMessage() . "\n";
+ exit(1);
+}
diff --git a/scratch/stage0_db_update.sql b/scratch/stage0_db_update.sql
new file mode 100644
index 0000000..ce2f2dd
--- /dev/null
+++ b/scratch/stage0_db_update.sql
@@ -0,0 +1,73 @@
+-- Stage 0: Database Updates for Mobile Support & Bulk Import
+
+-- 1. Update Users Table
+ALTER TABLE users ADD COLUMN phone VARCHAR(20) NULL AFTER email;
+ALTER TABLE users ADD COLUMN phone_hash VARCHAR(64) NULL AFTER phone;
+ALTER TABLE users ADD COLUMN pin_hash VARCHAR(255) NULL;
+ALTER TABLE users ADD COLUMN biometric_enabled BOOLEAN DEFAULT FALSE;
+ALTER TABLE users ADD INDEX idx_phone_hash (phone_hash);
+
+-- 2. User Devices Table
+CREATE TABLE user_devices (
+ id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
+ user_id CHAR(36) NOT NULL,
+ device_fingerprint VARCHAR(64) NOT NULL,
+ device_name VARCHAR(100),
+ platform ENUM('android','ios') NOT NULL,
+ app_version VARCHAR(20),
+ push_token TEXT NULL,
+ is_trusted BOOLEAN DEFAULT FALSE,
+ last_seen_at DATETIME,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ UNIQUE KEY uq_user_device (user_id, device_fingerprint)
+);
+
+-- 3. Invoice Batches Table
+CREATE TABLE invoice_batches (
+ id CHAR(36) PRIMARY KEY,
+ tenant_id CHAR(36) NOT NULL,
+ company_id CHAR(36) NOT NULL,
+ uploaded_by CHAR(36) NOT NULL,
+ total_images INT NOT NULL DEFAULT 0,
+ processed_images INT NOT NULL DEFAULT 0,
+ status ENUM('uploading','processing','done','partial_fail') DEFAULT 'uploading',
+ source ENUM('mobile_scan','web_upload','whatsapp') DEFAULT 'mobile_scan',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ completed_at DATETIME NULL,
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
+ FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
+ FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL
+);
+
+-- 4. Invoice Processing Queue Table
+CREATE TABLE invoice_processing_queue (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ batch_id CHAR(36) NOT NULL,
+ invoice_id CHAR(36) NULL,
+ tenant_id CHAR(36) NOT NULL,
+ image_path VARCHAR(500) NOT NULL,
+ status ENUM('pending','processing','done','failed') DEFAULT 'pending',
+ attempts INT DEFAULT 0,
+ error_message TEXT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ processed_at DATETIME NULL,
+ INDEX idx_status_tenant (status, tenant_id),
+ INDEX idx_batch (batch_id)
+);
+
+-- 5. Excel Imports Table
+CREATE TABLE excel_imports (
+ id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
+ tenant_id CHAR(36) NOT NULL,
+ company_id CHAR(36) NOT NULL,
+ uploaded_by CHAR(36) NOT NULL,
+ filename VARCHAR(255) NOT NULL,
+ total_rows INT DEFAULT 0,
+ success_rows INT DEFAULT 0,
+ failed_rows INT DEFAULT 0,
+ status ENUM('processing','done','failed') DEFAULT 'processing',
+ error_log JSON NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
+);
diff --git a/scripts/PROJECT_DOCUMENTATION.md b/scripts/PROJECT_DOCUMENTATION.md
index f8c5493..6eefa8d 100644
--- a/scripts/PROJECT_DOCUMENTATION.md
+++ b/scripts/PROJECT_DOCUMENTATION.md
@@ -111,6 +111,134 @@ try {
```
+## File: `migrate_payments.php`
+
+```php
+exec("
+ CREATE TABLE IF NOT EXISTS subscription_plans (
+ id VARCHAR(50) PRIMARY KEY,
+ name_ar VARCHAR(255) NOT NULL,
+ name_en VARCHAR(255) NOT NULL,
+ max_companies INT NOT NULL,
+ max_invoices_month INT NOT NULL,
+ max_users INT NOT NULL,
+ price_jod DECIMAL(10,3) NOT NULL,
+ ai_features BOOLEAN DEFAULT TRUE,
+ jofotara_enabled BOOLEAN DEFAULT TRUE,
+ sort_order INT DEFAULT 0,
+ is_active BOOLEAN DEFAULT TRUE,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ ");
+
+ // 2. Insert initial plans
+ echo "Inserting initial plans...\n";
+ $plans = require __DIR__ . '/../app/config/plans.php';
+ $stmt = $db->prepare("
+ INSERT INTO subscription_plans (id, name_ar, name_en, max_companies, max_invoices_month, max_users, price_jod, ai_features, jofotara_enabled, sort_order)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON DUPLICATE KEY UPDATE
+ name_ar = VALUES(name_ar),
+ price_jod = VALUES(price_jod),
+ max_companies = VALUES(max_companies),
+ max_invoices_month = VALUES(max_invoices_month)
+ ");
+
+ $order = 0;
+ foreach ($plans as $id => $plan) {
+ $stmt->execute([
+ $id,
+ $plan['name_ar'],
+ $plan['name_en'],
+ $plan['max_companies'],
+ $plan['max_invoices_month'],
+ $plan['max_users'],
+ $plan['price_jod'],
+ $plan['ai_features'] ? 1 : 0,
+ $plan['jofotara_enabled'] ? 1 : 0,
+ $order++
+ ]);
+ }
+
+ // 3. Create payment_requests table
+ echo "Creating payment_requests table...\n";
+ $db->exec("
+ CREATE TABLE IF NOT EXISTS payment_requests (
+ id CHAR(36) PRIMARY KEY,
+ tenant_id CHAR(36) NOT NULL,
+ user_id CHAR(36) NOT NULL,
+ plan_id VARCHAR(50) NOT NULL,
+ amount_jod DECIMAL(10,3) NOT NULL,
+ internal_reference VARCHAR(50) UNIQUE NOT NULL,
+ cliq_alias VARCHAR(100) NOT NULL,
+ payer_name VARCHAR(255) DEFAULT NULL,
+ bank_reference VARCHAR(100) DEFAULT NULL,
+ status ENUM('pending','uploaded','verified','approved','rejected') DEFAULT 'pending',
+ admin_notes TEXT DEFAULT NULL,
+ verified_at DATETIME DEFAULT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id),
+ FOREIGN KEY (user_id) REFERENCES users(id),
+ INDEX idx_status (status),
+ INDEX idx_bank_ref (bank_reference)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ ");
+
+ // 4. Create bank_transactions table
+ echo "Creating bank_transactions table...\n";
+ $db->exec("
+ CREATE TABLE IF NOT EXISTS bank_transactions (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ bank_reference VARCHAR(100) UNIQUE NOT NULL,
+ amount DECIMAL(10,3) NOT NULL,
+ sender_name VARCHAR(255) DEFAULT NULL,
+ raw_message TEXT NOT NULL,
+ is_claimed BOOLEAN DEFAULT FALSE,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ INDEX idx_ref (bank_reference)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ ");
+
+ // 5. Update subscriptions table if needed
+ echo "Updating subscriptions table schema...\n";
+ // Check if column plan_id exists, if not add it
+ $cols = $db->query("SHOW COLUMNS FROM subscriptions")->fetchAll(PDO::FETCH_COLUMN);
+ if (!in_array('plan_id', $cols)) {
+ $db->exec("ALTER TABLE subscriptions ADD COLUMN plan_id VARCHAR(50) AFTER tenant_id");
+ $db->exec("ALTER TABLE subscriptions MODIFY COLUMN plan ENUM('free','basic','office','pro','enterprise') DEFAULT 'free'");
+ }
+ if (!in_array('max_users', $cols)) {
+ $db->exec("ALTER TABLE subscriptions ADD COLUMN max_users INT NOT NULL DEFAULT 1 AFTER max_invoices_per_month");
+ }
+
+ echo "Migration completed successfully!\n";
+
+} catch (\Throwable $e) {
+ echo "Migration failed: " . $e->getMessage() . "\n";
+ exit(1);
+}
+
+```
+
## File: `list_users.php`
```php