From 3b5f490efc97872b4da5db69f0969ff0234f91b8 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Thu, 7 May 2026 15:49:13 +0300 Subject: [PATCH] Update: 2026-05-07 15:49:13 --- composer.json | 3 +- .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 +++ .../Assets.xcassets/Contents.json | 6 + .../WidgetBackground.colorset/Contents.json | 11 + .../ios/MusadaqLiveActivity/Info.plist | 11 + .../MusadaqLiveActivity.swift | 84 ++++++ .../MusadaqLiveActivityBundle.swift | 86 ++++++ .../MusadaqLiveActivityControl.swift | 54 ++++ .../MusadaqLiveActivityLiveActivity.swift | 80 ++++++ .../MusadaqLiveActivityExtension.entitlements | 10 + .../ios/Runner.xcodeproj/project.pbxproj | 265 +++++++++++++++++- musadaq-app/ios/Runner/Info.plist | 6 + musadaq-app/ios/Runner/Runner.entitlements | 10 + scratch/run_migration.php | 22 ++ scratch/stage0_db_update.sql | 73 +++++ scripts/PROJECT_DOCUMENTATION.md | 128 +++++++++ 17 files changed, 889 insertions(+), 6 deletions(-) create mode 100644 musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/Contents.json create mode 100644 musadaq-app/ios/MusadaqLiveActivity/Assets.xcassets/WidgetBackground.colorset/Contents.json create mode 100644 musadaq-app/ios/MusadaqLiveActivity/Info.plist create mode 100644 musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivity.swift create mode 100644 musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityBundle.swift create mode 100644 musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityControl.swift create mode 100644 musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityLiveActivity.swift create mode 100644 musadaq-app/ios/MusadaqLiveActivityExtension.entitlements create mode 100644 musadaq-app/ios/Runner/Runner.entitlements create mode 100644 scratch/run_migration.php create mode 100644 scratch/stage0_db_update.sql 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