commit 507e31d7f9bd904c7e43c4d99ea2626e3642b72a Author: Sendi Guntur Fajar Saputra Date: Mon Jul 27 12:19:45 2026 +0700 Upload project Mylaundry1 diff --git a/laundry-app/.editorconfig b/laundry-app/.editorconfig new file mode 100644 index 0000000..a186cd2 --- /dev/null +++ b/laundry-app/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[compose.yaml] +indent_size = 4 diff --git a/laundry-app/.gitattributes b/laundry-app/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/laundry-app/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/laundry-app/.gitignore b/laundry-app/.gitignore new file mode 100644 index 0000000..b71b1ea --- /dev/null +++ b/laundry-app/.gitignore @@ -0,0 +1,24 @@ +*.log +.DS_Store +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +/.fleet +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +Homestead.json +Homestead.yaml +Thumbs.db diff --git a/laundry-app/README.md b/laundry-app/README.md new file mode 100644 index 0000000..0165a77 --- /dev/null +++ b/laundry-app/README.md @@ -0,0 +1,59 @@ +

Laravel Logo

+ +

+Build Status +Total Downloads +Latest Stable Version +License +

+ +## About Laravel + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## Learning Laravel + +Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application. + +If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. + +## Laravel Sponsors + +We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). + +### Premium Partners + +- **[Vehikl](https://vehikl.com)** +- **[Tighten Co.](https://tighten.co)** +- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** +- **[64 Robots](https://64robots.com)** +- **[Curotec](https://www.curotec.com/services/technologies/laravel)** +- **[DevSquad](https://devsquad.com/hire-laravel-developers)** +- **[Redberry](https://redberry.international/laravel-development)** +- **[Active Logic](https://activelogic.com)** + +## Contributing + +Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. + +## License + +The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/laundry-app/app/Exports/ReportExport.php b/laundry-app/app/Exports/ReportExport.php new file mode 100644 index 0000000..263987e --- /dev/null +++ b/laundry-app/app/Exports/ReportExport.php @@ -0,0 +1,305 @@ +startDate = $startDate; + $this->endDate = $endDate; + $this->deliveryType = $deliveryType; + $this->paymentMethod = $paymentMethod; + $this->ids = $ids; + } + + public function startCell(): string + { + return 'A8'; + } + + public function query() + { + $query = \App\Models\Order::query()->whereIn('status', ['DIAMBIL', 'DITERIMA']); + + if (!empty($this->ids)) { + $query->whereIn('id', $this->ids); + } else { + $query->whereBetween('updated_at', [$this->startDate . ' 00:00:00', $this->endDate . ' 23:59:59']); + + if ($this->deliveryType !== 'all') { + if ($this->deliveryType === 'none') { + $query->where(function($q) { + $q->where('delivery_type', 'none')->orWhereNull('delivery_type'); + }); + } else { + $query->where('delivery_type', $this->deliveryType); + } + } + + if ($this->paymentMethod !== 'all') { + $query->where('payment_method', $this->paymentMethod); + } + } + + return $query; + } + public function headings(): array + { + $headers = [ + 'No. Order', 'Tanggal Selesai', 'Nama Pelanggan', + 'Layanan', 'Jumlah', 'Harga Layanan', + 'Metode', 'Layanan Driver' + ]; + + if (strtoupper($this->paymentMethod) !== 'QRIS') { + $headers[] = 'Uang Masuk'; + $headers[] = 'Kembalian'; + } + + $headers[] = 'Ongkir'; + $headers[] = 'Total Harga'; + + return $headers; + } + + public function map($order): array + { + $this->rowCount++; + + // 1. Ambil Data Layanan dari Order Items + $layananArr = []; + $jumlahArr = []; + $hargaLayananArr = []; + + if ($order->items) { + foreach ($order->items as $item) { + $layananArr[] = $item->service_name; + + // Format jumlah agar tidak ada 0 berlebih di belakang koma (misal 2.000 -> 2) + $qty = floatval($item->qty_or_weight); + $qtyStr = rtrim(rtrim(number_format($qty, 3, '.', ''), '0'), '.'); + $jumlahArr[] = $qtyStr . ' ' . $item->unit; + + $hargaLayananArr[] = $item->price; + } + } + + $layananStr = implode(",\n", $layananArr); + $jumlahStr = implode(",\n", $jumlahArr); + $hargaLayananStr = implode(",\n", $hargaLayananArr); + + // 2. Bersihkan Angka + $valTotal = (int) preg_replace('/[^0-9]/', '', $order->total_price); + $valReceived = (int) preg_replace('/[^0-9]/', '', $order->cash_received ?? $order->total_price); + $valChange = (int) preg_replace('/[^0-9]/', '', $order->cash_change ?? 0); + $valOngkir = (int) ($order->delivery_fee ?? 0); + + $method = strtoupper($order->payment_method); + + // 3. Klasifikasi Pendapatan + if ($method == 'CASH') { + $this->totalCash += $valTotal; + $uangMasuk = $valReceived; + $kembalian = $valChange; + } else { + $this->totalQRIS += $valTotal; + $uangMasuk = $valTotal; + $kembalian = 0; + } + + $this->totalDeliveryFee += $valOngkir; + $this->grandTotalHarga += $valTotal; + $this->totalUangMasuk += $uangMasuk; + $this->totalKembalian += $kembalian; + + $typeLabel = "Ambil Sendiri"; + if ($order->delivery_type === 'pickup') $typeLabel = "Pick-up"; + else if ($order->delivery_type === 'delivery') $typeLabel = "Delivery"; + else if ($order->delivery_type === 'both') $typeLabel = "Delivery & Pick-up"; + + // Return data dengan kolom baru + $row = [ + '#' . ($order->order_number ?? $order->invoice_number), + $order->updated_at->format('d/m/Y H:i'), + $order->customer_name, + $layananStr, + $jumlahStr, + $hargaLayananStr, + $method == 'CASH' ? 'TUNAI' : $method, + $typeLabel, + ]; + + if (strtoupper($this->paymentMethod) !== 'QRIS') { + $row[] = $uangMasuk; + $row[] = $kembalian; + } + + $row[] = $valOngkir; + $row[] = $valTotal; + + return $row; + } + + public function registerEvents(): array + { + return [ + AfterSheet::class => function(AfterSheet $event) { + $lastDataRow = $this->rowCount + 8; + $summaryBottomRow = $lastDataRow + 1; + + $isQris = strtoupper($this->paymentMethod) === 'QRIS'; + $lastCol = $isQris ? 'J' : 'L'; + + // --- BAGIAN ATAS: JUDUL (Disesuaikan ke kolom L atau J) --- + $title = 'LAPORAN PENDAPATAN LAUNDRY'; + $filters = []; + + if ($this->deliveryType !== 'all') { + $deliveryLabel = ''; + if ($this->deliveryType === 'none') $deliveryLabel = 'Ambil Sendiri'; + else if ($this->deliveryType === 'pickup') $deliveryLabel = 'Pick-up'; + else if ($this->deliveryType === 'delivery') $deliveryLabel = 'Delivery'; + else if ($this->deliveryType === 'both') $deliveryLabel = 'Delivery & Pick-up'; + + if ($deliveryLabel) $filters[] = $deliveryLabel; + } + + if ($this->paymentMethod !== 'all') { + $paymentLabel = strtoupper($this->paymentMethod); + if ($paymentLabel === 'CASH') $paymentLabel = 'TUNAI'; + $filters[] = $paymentLabel; + } + + if (!empty($filters)) { + $title .= ' (' . implode(' - ', $filters) . ')'; + } + + $event->sheet->mergeCells("A1:{$lastCol}1"); + $event->sheet->setCellValue('A1', $title); + + $periode = Carbon::parse($this->startDate)->format('d M Y') . ' - ' . Carbon::parse($this->endDate)->format('d M Y'); + $event->sheet->mergeCells("A2:{$lastCol}2"); + $event->sheet->setCellValue('A2', 'Periode: ' . $periode); + + // Ringkasan Pendapatan di Atas (Baris 4-6) + $event->sheet->setCellValue('A4', 'TOTAL KESELURUHAN'); + $event->sheet->setCellValue('B4', ': Rp ' . number_format($this->grandTotalHarga, 0, ',', '.')); + + $event->sheet->setCellValue('A5', 'TOTAL JASA LAUNDRY'); + $event->sheet->setCellValue('B5', ': Rp ' . number_format($this->grandTotalHarga - $this->totalDeliveryFee, 0, ',', '.')); + + $event->sheet->setCellValue('A6', 'TOTAL ONGKIR (DRIVER)'); + $event->sheet->setCellValue('B6', ': Rp ' . number_format($this->totalDeliveryFee, 0, ',', '.')); + + $currentRow = 4; + if ($this->paymentMethod === 'all' || strtoupper($this->paymentMethod) === 'CASH') { + $event->sheet->setCellValue('D' . $currentRow, 'TOTAL TUNAI (Fisik)'); + $event->sheet->setCellValue('E' . $currentRow, ': Rp ' . number_format($this->totalCash, 0, ',', '.')); + $currentRow++; + } + + if ($this->paymentMethod === 'all' || strtoupper($this->paymentMethod) === 'QRIS') { + $event->sheet->setCellValue('D' . $currentRow, 'TOTAL QRIS (Digital)'); + $event->sheet->setCellValue('E' . $currentRow, ': Rp ' . number_format($this->totalQRIS, 0, ',', '.')); + $currentRow++; + } + + // --- BAGIAN BAWAH: TOTAL KESELURUHAN KOLOM --- + $event->sheet->mergeCells("A{$summaryBottomRow}:H{$summaryBottomRow}"); + $event->sheet->setCellValue("A{$summaryBottomRow}", "TOTAL KESELURUHAN"); + if ($isQris) { + $event->sheet->setCellValue("I{$summaryBottomRow}", $this->totalDeliveryFee); + $event->sheet->setCellValue("J{$summaryBottomRow}", $this->grandTotalHarga); + } else { + $event->sheet->setCellValue("I{$summaryBottomRow}", ""); + $event->sheet->setCellValue("J{$summaryBottomRow}", ""); + $event->sheet->setCellValue("K{$summaryBottomRow}", $this->totalDeliveryFee); + $event->sheet->setCellValue("L{$summaryBottomRow}", $this->grandTotalHarga); + } + + // --- STYLING --- + $event->sheet->getStyle('A1:A2')->getFont()->setBold(true)->setSize(14); + $event->sheet->getStyle('A1:A2')->getAlignment()->setHorizontal('center'); + + $event->sheet->getStyle('A4:A6')->getFont()->setBold(true); + $lastRowStyle = $currentRow - 1; + if ($lastRowStyle >= 4) { + $event->sheet->getStyle('D4:D' . $lastRowStyle)->getFont()->setBold(true); + $event->sheet->getStyle('E4:E' . $lastRowStyle)->getFont()->getColor()->setRGB('1D6F42'); + } + $event->sheet->getStyle('B4:B6')->getFont()->getColor()->setRGB('1D6F42'); + + // Styling Baris Total Bawah + $event->sheet->getStyle("A{$summaryBottomRow}:{$lastCol}{$summaryBottomRow}")->applyFromArray([ + 'font' => ['bold' => true], + 'fill' => ['fillType' => 'solid', 'startColor' => ['rgb' => 'E2EFDA']], // Warna hijau muda yang lebih rapi + 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]] + ]); + $event->sheet->getStyle("A{$summaryBottomRow}")->getAlignment()->setHorizontal('left'); + if ($isQris) { + $event->sheet->getStyle("I{$summaryBottomRow}:J{$summaryBottomRow}")->getNumberFormat()->setFormatCode('#,##0'); + } else { + $event->sheet->getStyle("I{$summaryBottomRow}:L{$summaryBottomRow}")->getNumberFormat()->setFormatCode('#,##0'); + } + + // Set wrap text agar comma-separated dengan \n tampil rapi + $event->sheet->getStyle("D8:F{$lastDataRow}")->getAlignment()->setWrapText(true); + }, + ]; + } + + public function styles(Worksheet $sheet) + { + $highestRow = $sheet->getHighestRow(); + $isQris = strtoupper($this->paymentMethod) === 'QRIS'; + $lastCol = $isQris ? 'J' : 'L'; + + // Header Tabel (Baris 8, disesuaikan sampai kolom L atau J) + $sheet->getStyle("A8:{$lastCol}8")->applyFromArray([ + 'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']], + 'fill' => ['fillType' => 'solid', 'startColor' => ['rgb' => '1D6F42']], + 'alignment' => ['horizontal' => 'center'], + ]); + + // Border Tabel + $sheet->getStyle("A8:{$lastCol}" . $highestRow)->getBorders()->getAllBorders()->setBorderStyle(Border::BORDER_THIN); + + // Format Ribuan Kolom Keuangan + $sheet->getStyle('F9:F' . $highestRow)->getNumberFormat()->setFormatCode('#,##0'); + if ($isQris) { + $sheet->getStyle('I9:J' . $highestRow)->getNumberFormat()->setFormatCode('#,##0'); + } else { + $sheet->getStyle('I9:L' . $highestRow)->getNumberFormat()->setFormatCode('#,##0'); + } + + return []; + } +} \ No newline at end of file diff --git a/laundry-app/app/Http/Controllers/Api/AuthController.php b/laundry-app/app/Http/Controllers/Api/AuthController.php new file mode 100644 index 0000000..d742850 --- /dev/null +++ b/laundry-app/app/Http/Controllers/Api/AuthController.php @@ -0,0 +1,58 @@ +validate([ + 'email' => 'required|email', + 'password' => 'required', + ]); + + $user = User::where('email', $request->email)->first(); + + if (!$user || !Hash::check($request->password, $user->password)) { + return response()->json(['message' => 'Email atau password salah.'], 401); + } + + // --- PROTEKSI API --- + if (!in_array($user->role, ['staf', 'driver'])) { + return response()->json([ + 'message' => 'Akses ditolak. Aplikasi ini hanya untuk staf dan driver.' + ], 403); // 403 Forbidden + } + + // --- PERBAIKAN 1: Hapus token lama sebelum membuat yang baru --- + // Ini akan mencegah tabel personal_access_tokens menjadi penuh/numpuk + $user->tokens()->delete(); + + $token = $user->createToken('auth_token')->plainTextToken; + + return response()->json([ + 'success' => true, + 'access_token' => $token, + 'token_type' => 'Bearer', + 'user' => $user, + 'shop' => $user->getOwnerShop(), + ]); + } + + // --- PERBAIKAN 2: Tambahkan fungsi Logout --- + public function logout(Request $request) + { + // Menghapus token yang sedang dipakai oleh staf saat ini + $request->user()->currentAccessToken()->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Berhasil logout dan token telah dihapus.' + ]); + } +} \ No newline at end of file diff --git a/laundry-app/app/Http/Controllers/Api/DashboardController.php b/laundry-app/app/Http/Controllers/Api/DashboardController.php new file mode 100644 index 0000000..822e379 --- /dev/null +++ b/laundry-app/app/Http/Controllers/Api/DashboardController.php @@ -0,0 +1,150 @@ + (int) Order::where('status', 'ANTRIAN')->count(), + 'proses' => (int) Order::where('status', 'PROSES')->count(), + 'selesai' => (int) Order::where('status', 'SELESAI')->count(), + ]; + + return response()->json([ + 'success' => true, + 'data' => $stats + ], 200); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil stats: ' . $e->getMessage() + ], 500); + } + } + + public function getActivities() + { + try { + // PERBAIKAN: Tambahkan estimation_time, created_at di dalam select + $activities = Order::with('items')->select('id', 'order_number', 'customer_name', 'phone', 'status', 'updated_at', 'delivery_fee', 'total_price', 'delivery_type', 'payment_method', 'cash_received', 'estimation_time', 'created_at') + ->orderBy('updated_at', 'desc') + ->limit(50) + ->get() + ->map(function ($order) { + $services = $order->items->map(function ($item) { + $qty = fmod($item->qty_or_weight, 1) == 0 ? (int)$item->qty_or_weight : $item->qty_or_weight; + return $item->service_name . ' (' . $qty . ' ' . $item->unit . ')'; + })->implode(' + '); + + $subtitle = $services; + if ($order->delivery_fee > 0) { + $ongkir = number_format($order->delivery_fee, 0, ',', '.'); + $subtitle .= ($subtitle ? " + " : "") . "Ongkir: Rp $ongkir"; + } + $isPaid = strtoupper($order->payment_method) !== 'BAYAR NANTI' || $order->cash_received > 0; + // Calculate estimation type + $tipeEstimasi = "Reguler"; + if ($order->estimation_time && $order->created_at) { + $diffInHours = $order->created_at->diffInHours($order->estimation_time); + if ($diffInHours <= 6) { + $tipeEstimasi = "Kilat"; + } elseif ($diffInHours <= 24) { + $tipeEstimasi = "Ekspres"; + } + } + + return [ + 'id' => $order->id, + 'order_number' => $order->order_number, + 'title' => (string) $order->customer_name, + 'subtitle' => $subtitle, + 'phone' => $order->phone ?? "-", + 'time' => $order->updated_at ? $order->updated_at->diffForHumans() : '-', + 'status' => (string) $order->status, + 'total' => $order->total_price, + 'delivery_type' => $order->delivery_type ?? 'none', + 'payment_status' => $isPaid ? 'PAID' : 'UNPAID', + 'payment_method' => $order->payment_method, + 'cash_received' => $order->cash_received, + 'estimation_time'=> $order->estimation_time ? $order->estimation_time->toDateTimeString() : '', + 'estimation_type'=> $tipeEstimasi, + ]; + }); + + return response()->json([ + 'success' => true, + 'data' => $activities + ], 200); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil aktivitas: ' . $e->getMessage() + ], 500); + } + } + + public function getOrdersByStatus($status) + { + try { + $orders = Order::with('items')->where('status', strtoupper($status)) + ->latest() + ->get() + ->map(function ($order) { + $rawService = $order->items->map(function ($item) { + return trim($item->service_name); + })->implode(', '); + + $berat = $order->total_weight ?? '0'; + $jasa = $rawService; + + // --- PERBAIKAN DI SINI --- + // Ambil data langsung dari kolom estimation_time di database + // Jika null, kirim string kosong agar ditangani Flutter sebagai "Belum diatur" + $estimationTime = $order->estimation_time ? $order->estimation_time->toDateTimeString() : ''; + + $tipeEstimasi = "Reguler"; + + // 1. Deteksi Tipe Estimasi berdasarkan selisih waktu + if ($order->estimation_time && $order->created_at) { + $diffInHours = $order->created_at->diffInHours($order->estimation_time); + if ($diffInHours <= 6) { + $tipeEstimasi = "Kilat"; + } elseif ($diffInHours <= 24) { + $tipeEstimasi = "Ekspres"; + } + } + + $isPaid = strtoupper($order->payment_method) !== 'BAYAR NANTI' || $order->cash_received > 0; + return [ + 'id' => $order->id, + 'order_number' => $order->order_number, + 'customer_name' => $order->customer_name, + 'phone' => $order->phone ?? "-", + 'status' => $order->status, + 'weight' => $berat, + 'service_name' => $jasa, + 'estimation_type' => $tipeEstimasi, + 'estimation_time' => $estimationTime, + 'total_price' => number_format($order->total_price ?? 0, 0, ',', '.'), + 'delivery_fee' => $order->delivery_fee ?? 0, + 'delivery_type' => $order->delivery_type ?? 'none', + 'payment_status' => $isPaid ? 'PAID' : 'UNPAID', + 'payment_method' => $order->payment_method, + 'cash_received' => $order->cash_received, + ]; + }); + + return response()->json(['success' => true, 'data' => $orders->values()->all()], 200); + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); + } + } +} diff --git a/laundry-app/app/Http/Controllers/Api/ServiceController.php b/laundry-app/app/Http/Controllers/Api/ServiceController.php new file mode 100644 index 0000000..b516ce7 --- /dev/null +++ b/laundry-app/app/Http/Controllers/Api/ServiceController.php @@ -0,0 +1,32 @@ +get(); + + return response()->json([ + 'success' => true, + 'message' => 'Daftar Jasa Aktif Berhasil Dimuat', + 'data' => $services + ], 200); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal memuat data', + 'error' => $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/laundry-app/app/Http/Controllers/Api/ShopSettingsController.php b/laundry-app/app/Http/Controllers/Api/ShopSettingsController.php new file mode 100644 index 0000000..9650fb0 --- /dev/null +++ b/laundry-app/app/Http/Controllers/Api/ShopSettingsController.php @@ -0,0 +1,96 @@ +getOwnerShop(); + + if (!$shop) { + return response()->json([ + 'status' => 'success', + 'data' => [ + 'shop_name' => null, + 'device_id' => null, + 'shop_latitude' => null, + 'shop_longitude' => null, + 'delivery_fee_per_km' => 0, + 'free_delivery_km' => 0, + 'is_delivery_active' => false, + 'is_estimation_active'=> false, + 'express_extra_price' => 0, + 'kilat_extra_price' => 0, + 'qris_image_url' => null, + ] + ], 200); + } + + return response()->json([ + 'status' => 'success', + 'data' => [ + 'shop_name' => $shop->shop_name, + 'device_id' => $shop->device_id, + 'shop_latitude' => $shop->latitude, + 'shop_longitude' => $shop->longitude, + 'delivery_fee_per_km' => $shop->delivery_fee_per_km, + 'free_delivery_km' => $shop->free_delivery_km ?? 0, + 'is_delivery_active' => $shop->is_delivery_active, + 'is_estimation_active'=> $shop->is_estimation_active ?? false, + 'express_extra_price' => $shop->express_extra_price ?? 0, + 'kilat_extra_price' => $shop->kilat_extra_price ?? 0, + 'qris_image_url' => $shop->qris_image ? asset('storage/' . $shop->qris_image) : null, + ] + ], 200); + } + + public function updateSettings(Request $request) + { + /** @var User $user */ + $user = Auth::user(); + $ownerId = $user->getOwnerId(); + + $validated = $request->validate([ + 'shop_name' => 'nullable|string|max:100', + 'device_id' => 'nullable|string|max:50', + 'shop_latitude' => 'nullable|numeric', + 'shop_longitude' => 'nullable|numeric', + 'delivery_fee_per_km' => 'required|integer|min:0', + 'free_delivery_km' => 'required|integer|min:0', + 'is_delivery_active' => 'required|boolean', + 'is_estimation_active'=> 'required|boolean', + 'express_extra_price' => 'required|integer|min:0', + 'kilat_extra_price' => 'required|integer|min:0', + ]); + + Shop::updateOrCreate( + ['user_id' => $ownerId], + [ + 'shop_name' => $validated['shop_name'] ?? null, + 'device_id' => $validated['device_id'] ?? null, + 'latitude' => $validated['shop_latitude'] ?? null, + 'longitude' => $validated['shop_longitude'] ?? null, + 'delivery_fee_per_km' => $validated['delivery_fee_per_km'], + 'free_delivery_km' => $validated['free_delivery_km'], + 'is_delivery_active' => $validated['is_delivery_active'], + 'is_estimation_active'=> $validated['is_estimation_active'], + 'express_extra_price' => $validated['express_extra_price'], + 'kilat_extra_price' => $validated['kilat_extra_price'], + ] + ); + + return response()->json([ + 'status' => 'success', + 'message' => 'Pengaturan toko berhasil disimpan.', + ], 200); + } +} diff --git a/laundry-app/app/Http/Controllers/Api/TransactionController.php b/laundry-app/app/Http/Controllers/Api/TransactionController.php new file mode 100644 index 0000000..4661466 --- /dev/null +++ b/laundry-app/app/Http/Controllers/Api/TransactionController.php @@ -0,0 +1,694 @@ +getOwnerId(); + + $orders = Order::with('items') + ->where('user_id', $ownerId) + ->orderBy('created_at', 'desc') + ->get() + ->map(function ($order) { + return [ + 'id' => $order->id, + 'order_number' => $order->order_number, + 'customer_name' => $order->customer_name, + 'phone' => $order->phone, + 'total_price' => $order->total_price, + 'status' => $order->status, + 'created_at' => $order->created_at, + ]; + }); + + return response()->json([ + 'status' => 'success', + 'data' => $orders + ], 200); + } + + // 1. Endpoint untuk Staf: Membuat Jadwal Jemput (Order Awal) + public function requestPickup(Request $request) + { + $validated = $request->validate([ + 'customer_name' => 'required|string', + 'phone' => 'required|string', + 'address' => 'nullable|string', + 'latitude' => 'nullable', + 'longitude' => 'nullable', + ]); + + /** @var User $user */ + $user = Auth::user(); + $ownerId = $user->getOwnerId(); + + $today = Carbon::today(); + $prefix = 'ORD-' . $today->format('Ymd') . '-' . $ownerId . '-'; + $lastOrder = Order::withoutGlobalScopes() + ->where('user_id', $ownerId) + ->where('order_number', 'LIKE', $prefix . '%') + ->orderBy('order_number', 'desc') + ->first(); + + $nextNum = 1; + if ($lastOrder) { + $lastNum = (int) substr($lastOrder->order_number, -3); + $nextNum = $lastNum + 1; + } + $orderNumber = $prefix . str_pad($nextNum, 3, '0', STR_PAD_LEFT); + + $order = Order::create([ + 'order_number' => $orderNumber, + 'user_id' => $ownerId, + 'customer_name' => $validated['customer_name'], + 'phone' => $validated['phone'], + 'address' => $validated['address'], + 'latitude' => $validated['latitude'], + 'longitude' => $validated['longitude'], + 'status' => 'PICKUP', // Status awal + 'total_price' => 0, + 'total_weight' => 0, + ]); + + return response()->json([ + 'status' => 'success', + 'message' => 'Jadwal jemput berhasil dibuat', + 'data' => $order + ], 201); + } + + // 2. Endpoint untuk Driver: Mengambil daftar tugas (Jemput & Antar) + public function getDriverTasks() + { + /** @var User $user */ + $user = Auth::user(); + $ownerId = $user->getOwnerId(); + + $tasks = Order::with('items')->where('user_id', $ownerId) + ->where(function($query) { + $query->whereIn('status', ['PICKUP', 'MENUNGGU JEMPUT', 'DI DRIVER', 'DELIVERY']) + ->orWhere(function($q) { + // Untuk kompatibilitas mundur jika ada pesanan lama + $q->where('status', 'SELESAI') + ->whereIn('delivery_type', ['delivery', 'both']); + }); + }) + ->orderBy('created_at', 'asc') + ->get() + ->map(function($order) { + $tipeEstimasi = "Reguler"; + if ($order->estimation_time && $order->created_at) { + $diffInHours = $order->created_at->diffInHours($order->estimation_time); + if ($diffInHours <= 6) $tipeEstimasi = "Kilat"; + elseif ($diffInHours <= 24) $tipeEstimasi = "Ekspres"; + } else { + $tipeEstimasi = "-"; + } + + $serviceNameStr = $order->items->map(function($item) { + $qtyStr = $item->unit === 'PCS' ? round($item->qty_or_weight) . ' PCS' : number_format($item->qty_or_weight, 3, '.', '') . ' KG'; + return "{$item->service_name} ({$qtyStr})||" . $item->subtotal; + })->implode("\n"); + + return [ + 'id' => $order->id, + 'order_number' => $order->order_number, + 'type_id' => (in_array($order->status, ['PICKUP', 'MENUNGGU JEMPUT']) ? 'P' : 'O') . $order->id, + 'task_type' => (in_array($order->status, ['PICKUP', 'MENUNGGU JEMPUT']) ? 'jemput' : 'antar'), + 'customer_name' => $order->customer_name, + 'phone' => $order->phone, + 'address' => $order->address, + 'latitude' => $order->latitude, + 'longitude' => $order->longitude, + 'status' => $order->status, + 'service_name' => $serviceNameStr, + 'weight' => $order->total_weight, + 'delivery_type' => $order->delivery_type, + 'delivery_fee' => $order->delivery_fee, + 'total_price' => $order->total_price, + 'payment_method'=> $order->payment_method, + 'cash_received' => $order->cash_received, + 'estimation_type'=> $tipeEstimasi, + ]; + }); + + return response()->json([ + 'status' => 'success', + 'data' => $tasks + ], 200); + } + + public function store(Request $request) + { + if (!$request->expectsJson()) { + return response()->json(['message' => 'Harus menggunakan header Accept application/json'], 406); + } + + $validated = $request->validate([ + 'customer_name' => 'required|string', + 'phone' => 'required|string', + 'items' => 'required|array', + 'items.*.service_id' => 'nullable', + 'items.*.service_name' => 'required|string', + 'items.*.price' => 'required|numeric', + 'items.*.qty_or_weight' => 'required|numeric', + 'items.*.unit' => 'required|string', + 'items.*.subtotal' => 'required|numeric', + 'total_price' => 'required', + 'total_weight' => 'required', + 'payment_method' => 'nullable|string', + 'cash_received' => 'nullable', + 'address' => 'nullable|string', + 'latitude' => 'nullable', + 'longitude' => 'nullable', + 'delivery_type' => 'nullable|string', + ]); + + /** @var User $user */ + $user = Auth::user(); + $ownerId = $user->getOwnerId(); + $shop = $user->getOwnerShop(); + + if (!$shop) { + // Jika shop belum ada, buat default (agar tidak error) + $shop = Shop::create(['user_id' => $ownerId]); + } + + $deliveryFee = 0; + $deliveryDistance = 0; + $deliveryType = $request->delivery_type ?? 'none'; + + $shop_lat = $shop->latitude; + $shop_lng = $shop->longitude; + + if ($deliveryType !== 'none' && $shop->is_delivery_active && + $request->latitude && $request->longitude) { + + try { + $url = "http://router.project-osrm.org/route/v1/driving/{$shop_lng},{$shop_lat};{$request->longitude},{$request->latitude}?overview=false"; + $osrmResponse = Http::timeout(5)->get($url); + + if ($osrmResponse->successful()) { + $osrmData = $osrmResponse->json(); + if (isset($osrmData['routes'][0]['distance'])) { + $deliveryDistance = round($osrmData['routes'][0]['distance'] / 1000, 2); + + // Logika Gratis KM dinamis dari shop setting + $freeDeliveryKm = $shop->free_delivery_km ?? 0; + if ($deliveryDistance <= $freeDeliveryKm) { + $deliveryFee = 0; + } else { + $chargeableDistance = $deliveryDistance - $freeDeliveryKm; + $baseFee = $chargeableDistance * $shop->delivery_fee_per_km; + $multiplier = ($deliveryType === 'both') ? 2 : 1; + $rawFee = $baseFee * $multiplier; + $deliveryFee = (int) (ceil($rawFee / 500) * 500); + } + } + } + } catch (\Exception $e) { + Log::error("OSRM Error: " . $e->getMessage()); + } + } + + $today = Carbon::today(); + $isUpdate = ($request->has('order_id') && !empty($request->order_id)); + $orderNumber = ""; + + // Fungsi untuk generate nomor urut berikutnya yang aman per owner + $getNextOrderNumber = function() use ($today, $ownerId) { + $prefix = 'ORD-' . $today->format('Ymd') . '-' . $ownerId . '-'; + $lastOrder = Order::withoutGlobalScopes() + ->where('user_id', $ownerId) + ->where('order_number', 'LIKE', $prefix . '%') + ->orderBy('order_number', 'desc') + ->first(); + + $nextNum = 1; + if ($lastOrder) { + $lastNum = (int) substr($lastOrder->order_number, -3); + $nextNum = $lastNum + 1; + } + return $prefix . str_pad($nextNum, 3, '0', STR_PAD_LEFT); + }; + + if (!$isUpdate) { + $orderNumber = $getNextOrderNumber(); + } + + $estType = strtolower($request->estimation_type ?? ''); + if (str_contains($estType, 'none')) { + $estimationTime = Carbon::now()->addDays(3); + } elseif (str_contains($estType, 'kilat')) { + $estimationTime = Carbon::now()->addHours(6); + } elseif (str_contains($estType, 'ekspres') || str_contains($estType, 'express')) { + $estimationTime = Carbon::now()->addDays(1); + } else { + $estimationTime = Carbon::now()->addDays(3); + } + + $cashReceived = intval($request->cash_received ?? 0); + $totalPrice = intval($validated['total_price']) + $deliveryFee; + $totalPrice = (int) (ceil($totalPrice / 500) * 500); // Bulatkan ke atas kelipatan 500 + $cashChange = $cashReceived - $totalPrice; + + try { + if ($isUpdate) { + $order = Order::find($request->order_id); + if ($order) { + $updateData = [ + 'total_price' => $totalPrice, + 'total_weight' => $validated['total_weight'], + 'status' => 'ANTRIAN', + 'estimation_time' => $estimationTime, + 'payment_method' => strtoupper($request->payment_method ?? 'CASH'), + 'cash_received' => $cashReceived, + 'cash_change' => $cashChange < 0 ? 0 : $cashChange, + 'delivery_type' => $deliveryType, + 'delivery_fee' => $deliveryFee, + 'delivery_distance' => $deliveryDistance, + ]; + + // Jika sebelumnya masih format PCK, ubah ke ORD + if (str_starts_with($order->order_number, 'PCK')) { + $updateData['order_number'] = $getNextOrderNumber(); + } + + $order->update($updateData); + + // Recreate items + $order->items()->delete(); + foreach ($validated['items'] as $item) { + $order->items()->create([ + 'service_id' => $item['service_id'] ?? null, + 'service_name' => $item['service_name'], + 'price' => $item['price'], + 'qty_or_weight' => $item['qty_or_weight'], + 'unit' => $item['unit'], + 'subtotal' => $item['subtotal'], + 'estimation_name' => $item['estimation_name'] ?? null, + ]); + } + } else { + return response()->json(['message' => 'Pesanan tidak ditemukan.'], 404); + } + } else { + $order = Order::create([ + 'order_number' => $orderNumber, + 'user_id' => $ownerId, + 'customer_name' => $validated['customer_name'], + 'phone' => $validated['phone'], + 'total_price' => $totalPrice, + 'total_weight' => $validated['total_weight'], + 'status' => 'ANTRIAN', + 'estimation_time' => $estimationTime, + 'payment_method' => strtoupper($request->payment_method ?? 'CASH'), + 'cash_received' => $cashReceived, + 'cash_change' => $cashChange < 0 ? 0 : $cashChange, + 'address' => $request->address, + 'latitude' => $request->latitude, + 'longitude' => $request->longitude, + 'delivery_type' => $deliveryType, + 'delivery_fee' => $deliveryFee, + 'delivery_distance' => $deliveryDistance, + ]); + + foreach ($validated['items'] as $item) { + $order->items()->create([ + 'service_id' => $item['service_id'] ?? null, + 'service_name' => $item['service_name'], + 'price' => $item['price'], + 'qty_or_weight' => $item['qty_or_weight'], + 'unit' => $item['unit'], + 'subtotal' => $item['subtotal'], + 'estimation_name' => $item['estimation_name'] ?? null, + ]); + } + } + } catch (\Exception $e) { + Log::error("Save Order Error: " . $e->getMessage()); + return response()->json(['message' => 'Gagal menyimpan ke database: ' . $e->getMessage()], 500); + } + + try { + $shopName = $shop->shop_name ?? 'My Laundry'; + $estimasiStr = $order->estimation_time ? $order->estimation_time->format('d/m/Y H:i') : ''; + + $tipeEstimasiLabel = "Reguler"; + if ($order->estimation_time && $order->created_at) { + $diffInHours = $order->created_at->diffInHours($order->estimation_time); + if ($diffInHours <= 6) $tipeEstimasiLabel = "Kilat"; + elseif ($diffInHours <= 24) $tipeEstimasiLabel = "Ekspres"; + } + + $metodeBayar = $order->payment_method; + $hargaStr = number_format($order->total_price, 0, ',', '.'); + $ongkirStr = number_format($order->delivery_fee, 0, ',', '.'); + $uangMasukStr = number_format($order->cash_received, 0, ',', '.'); + $kembalianStr = number_format($order->cash_change, 0, ',', '.'); + + // Format berat menjadi 1 angka di belakang koma (misal 3.12 -> 3.1) + $weightFormatted = number_format((float)$order->total_weight, 1, '.', ''); + + // Format Layanan secara dinamis (Selalu Baris Baru & Rata Kiri) + $serviceInfo = "๐Ÿ‘• *Layanan:*\n"; + + $i = 1; + foreach ($order->items as $item) { + $qtyStr = $item->unit === 'PCS' ? round($item->qty_or_weight) . ' PCS' : number_format($item->qty_or_weight, 3, '.', '') . ' KG'; + $priceStr = number_format($item->price, 0, ',', '.'); + $serviceInfo .= $i . ". {$item->service_name} ({$qtyStr}) - Rp $priceStr\n"; + $i++; + } + + $pesanBaru = "*NOTA $shopName*\n" + . "--------------------------------------\n" + . "Halo, ini konfirmasi atas pesanan bernama *{$order->customer_name}*.\n" + . "Pesanan laundry Anda telah kami terima! ๐Ÿ™\n\n" + . "๐Ÿงพ *No Order:* {$order->order_number}\n" + . $serviceInfo; + + if ($order->estimation_time) { + if (strtolower($request->estimation_type ?? '') === 'none') { + $pesanBaru .= "โฑ๏ธ *Estimasi Selesai:* $estimasiStr\n"; + } else { + $pesanBaru .= "โฑ๏ธ *Estimasi Selesai:* $tipeEstimasiLabel ($estimasiStr)\n"; + } + } + $pesanBaru .= "--------------------------------------\n"; + + // DETAIL PENGIRIMAN (Jika menggunakan jasa Driver) + if ($order->delivery_type !== 'none') { + $typeLabel = $this->getDeliveryTypeLabel($order->delivery_type); + + $pesanBaru .= "*DETAIL PENGIRIMAN*\n" + . "๐Ÿ›ต *Layanan:* $typeLabel\n"; + + if ($shop && $shop->is_delivery_active) { + $pesanBaru .= "๐Ÿ“ *Jarak:* {$order->delivery_distance} Km\n"; + } + + if ($order->delivery_fee > 0) { + $pesanBaru .= "๐Ÿšš *Ongkos Kirim:* Rp $ongkirStr\n"; + } else { + $pesanBaru .= "๐Ÿšš *Ongkos Kirim:* Gratis\n"; + } + $pesanBaru .= "--------------------------------------\n"; + } + + $pesanBaru .= "*RINCIAN PEMBAYARAN*\n" + . "๐Ÿ’ณ *Metode:* $metodeBayar\n" + . "๐Ÿ’ฐ *Total Tagihan:* *Rp $hargaStr*\n"; + + // Hanya paparkan Uang Masuk & Kembalian jika pelanggan membayar secara Tunai (CASH) + if ($metodeBayar === 'CASH') { + $pesanBaru .= "๐Ÿ’ต *Uang Tunai:* Rp $uangMasukStr\n" + . "๐Ÿ”„ *Kembalian:* Rp $kembalianStr\n"; + } + + $pesanBaru .= "--------------------------------------\n"; + if ($metodeBayar === 'BAYAR NANTI') { + $pesanBaru .= "Status Pembayaran: *BELUM LUNAS*\n"; + if (in_array(strtolower($order->delivery_type), ['delivery', 'both'])) { + $pesanBaru .= "Silakan melakukan pelunasan kepada Driver kami saat pesanan diantarkan.\n"; + } else { + $pesanBaru .= "Silakan melakukan pelunasan saat cucian diambil.\n"; + } + } else { + $pesanBaru .= "Status Pembayaran: *LUNAS*\n"; + } + + $pesanBaru .= "--------------------------------------\n" + . "Status saat ini: *DALAM ANTRIAN*\n" + . "Kami akan menginfokan kembali jika pesanan sudah diproses. Terima kasih!"; + + $this->sendWhatsAppMessage($order->phone, $pesanBaru); + } catch (\Exception $e) {} + + return response()->json([ + 'status' => 'success', + 'message' => 'Pesanan berhasil masuk', + 'data' => $order + ], 201); + } + + public function updateStatus(Request $request, $id) + { + $order = Order::find($id); + if (!$order) return response()->json(['message' => 'Data tidak ditemukan'], 404); + + $currentStatus = strtoupper($order->status); + $nextStatus = $currentStatus; + + if ($currentStatus === 'PICKUP' || $currentStatus === 'MENUNGGU JEMPUT') $nextStatus = 'TIBA DI TOKO'; + elseif ($currentStatus === 'TIBA DI TOKO') $nextStatus = 'ANTRIAN'; + elseif ($currentStatus === 'ANTRIAN') $nextStatus = 'PROSES'; + elseif ($currentStatus === 'PROSES') $nextStatus = 'SELESAI'; + elseif ($currentStatus === 'SELESAI') { + if (in_array(strtolower($order->delivery_type), ['delivery', 'both'])) { + $nextStatus = 'DELIVERY'; + } else { + $nextStatus = 'DIAMBIL'; + } + } + elseif ($currentStatus === 'DELIVERY' || $currentStatus === 'DI DRIVER') $nextStatus = 'DITERIMA'; + + $order->status = $nextStatus; + + // Cek jika ada data pembayaran tambahan (saat pelunasan) + if ($request->has('payment_method')) { + $order->payment_method = $request->payment_method; + $order->cash_received = $request->cash_received; + $order->cash_change = ($request->cash_received - $order->total_price); + } + + $order->save(); + + // --- KIRIM WA OTOMATIS SAAT STATUS BERUBAH --- + try { + /** @var User $shopUser */ + $shopUser = Auth::user(); + $shop = $shopUser->getOwnerShop(); + $shopName = $shop->shop_name ?? 'My Laundry'; + $namaPelanggan = $order->customer_name; + $pesanUpdate = "*UPDATE $shopName*\n\nHalo, ini update atas pesanan bernama *{$namaPelanggan}*.\n"; + + if ($nextStatus === 'PROSES') { + $pesanUpdate .= "Pesanan (No: *{$order->order_number}*) saat ini sedang *DIPROSES/DICUCI* ๐Ÿซง\nKami akan infokan kembali jika sudah selesai."; + } elseif ($nextStatus === 'SELESAI') { + $pesanUpdate .= "Pesanan (No: *{$order->order_number}*) sudah *SELESAI* dikerjakan! ๐ŸŽ‰\n\n"; + + $isDelivery = in_array(strtolower($order->delivery_type), ['delivery', 'both']); + + // Jika belum dibayar lunas + if (strtoupper($order->payment_method) === 'BAYAR NANTI' && $order->cash_received == 0) { + $hargaStr = number_format($order->total_price, 0, ',', '.'); + if ($isDelivery) { + $pesanUpdate .= "Tagihan sebesar *Rp $hargaStr* dapat dilunasi kepada Driver kami saat pesanan tiba nanti.\n\n"; + } else { + $pesanUpdate .= "Tagihan sebesar *Rp $hargaStr* dapat dilunasi saat pengambilan ya.\n\n"; + } + } + + if ($isDelivery) { + $pesanUpdate .= "Kami akan segera tugaskan driver untuk mengantar pesanan Anda. Mohon ditunggu ya!"; + } else { + $pesanUpdate .= "Silakan ambil cucian Anda di toko kami. Ditunggu kedatangannya!"; + } + } elseif ($nextStatus === 'DI DRIVER' || $nextStatus === 'DELIVERY') { + $pesanUpdate .= "Kami sudah menugaskan driver untuk mengantarkan Pesanan (No: *{$order->order_number}*) ke lokasi Anda! ๐Ÿ›ต๐Ÿ’จ\n\n"; + if (strtoupper($order->payment_method) === 'BAYAR NANTI' && $order->cash_received == 0) { + $hargaStr = number_format($order->total_price, 0, ',', '.'); + $pesanUpdate .= "Jangan lupa siapkan pembayaran sebesar *Rp $hargaStr* untuk diberikan kepada Driver kami ya.\n\n"; + } + $pesanUpdate .= "Mohon ditunggu. Terima kasih! ๐Ÿ™"; + } elseif ($nextStatus === 'TIBA DI TOKO') { + $pesanUpdate = "Halo *$namaPelanggan*,\n\n" + . "Pesanan Anda (No: *{$order->order_number}*) sudah sampai di toko kami dan akan segera kami input. โœ…\n\n" + . "Terima kasih! ๐Ÿ™"; + } elseif ($nextStatus === 'DIAMBIL' || $nextStatus === 'DITERIMA') { + if ($nextStatus === 'DITERIMA') { + $pesanUpdate .= "Pesanan (No: *{$order->order_number}*) telah *DITERIMA* oleh Anda.\n"; + } else { + $pesanUpdate .= "Pesanan (No: *{$order->order_number}*) telah *DIAMBIL*. โœ…\n"; + } + $pesanUpdate .= "Terima kasih telah mempercayakan cucian Anda di $shopName! ๐Ÿ™"; + } + + if ($nextStatus !== $currentStatus) { + $this->sendWhatsAppMessage($order->phone, $pesanUpdate); + } + } catch (\Exception $e) { + Log::error("WhatsApp Update Error: " . $e->getMessage()); + } + // --------------------------------------------- + + return response()->json(['status' => 'success', 'message' => 'Status diperbarui']); + } + + public function getOrdersByStatus($status) + { + /** @var User $user */ + $user = Auth::user(); + $ownerId = $user->getOwnerId(); + + $orders = Order::with('items')->where('user_id', $ownerId) + ->where('status', strtoupper($status)) + ->get() + ->map(function ($order) { + $tipeEstimasi = "Reguler"; + if ($order->estimation_time && $order->created_at) { + $diffInHours = $order->created_at->diffInHours($order->estimation_time); + if ($diffInHours <= 6) $tipeEstimasi = "Kilat"; + elseif ($diffInHours <= 24) $tipeEstimasi = "Ekspres"; + } else { + $tipeEstimasi = "-"; + } + + $order->estimation_type = $tipeEstimasi; + $order->weight = $order->total_weight; + $order->service_name = $order->items->map(function($item) { + $qtyStr = $item->unit === 'PCS' ? round($item->qty_or_weight) . ' PCS' : number_format($item->qty_or_weight, 3, '.', '') . ' KG'; + return "{$item->service_name} ({$qtyStr}) - Rp " . number_format($item->price, 0, ',', '.'); + })->implode("\n"); + + return $order; + }); + + return response()->json(['status' => 'success', 'data' => $orders], 200); + } + + public function checkDeliveryFee(Request $request) + { + /** @var User $user */ + $user = Auth::user(); + $shop = $user->getOwnerShop(); + + if (!$shop || !$shop->is_delivery_active) { + return response()->json(['delivery_fee' => 0, 'distance' => 0]); + } + + $lat = $request->latitude; + $lng = $request->longitude; + $deliveryType = $request->delivery_type; // 'pickup', 'delivery', 'both' + + $shop_lat = $shop->latitude; + $shop_lng = $shop->longitude; + + if (!$lat || !$lng) { + return response()->json(['delivery_fee' => 0, 'distance' => 0]); + } + + try { + $url = "http://router.project-osrm.org/route/v1/driving/{$shop_lng},{$shop_lat};{$lng},{$lat}?overview=false"; + $response = Http::timeout(5)->get($url); + + if ($response->successful()) { + $data = $response->json(); + if (isset($data['routes'][0]['distance'])) { + $distance = round($data['routes'][0]['distance'] / 1000, 2); + + // Logika Gratis KM dinamis dari shop setting + $freeDeliveryKm = $shop->free_delivery_km ?? 0; + if ($distance <= $freeDeliveryKm) { + $finalFee = 0; + } else { + $chargeableDistance = $distance - $freeDeliveryKm; + $baseFee = $chargeableDistance * $shop->delivery_fee_per_km; + + $multiplier = ($deliveryType === 'both') ? 2 : 1; + $rawFee = $baseFee * $multiplier; + + // Pembulatan kelipatan 500 ke atas + $finalFee = (int) (ceil($rawFee / 500) * 500); + } + + return response()->json([ + 'delivery_fee' => $finalFee, + 'distance' => $distance + ]); + } + } + } catch (\Exception $e) { + return response()->json(['delivery_fee' => 0, 'distance' => 0, 'error' => $e->getMessage()], 500); + } + + return response()->json(['delivery_fee' => 0, 'distance' => 0]); + } + + public function sendManualWhatsApp(Request $request) + { + $request->validate([ + 'phone' => 'required', + 'task_type' => 'required', + 'customer_name' => 'required' + ]); + + $customerName = $request->customer_name; + /** @var User $shopUser */ + $shopUser = Auth::user(); + $shop = $shopUser->getOwnerShop(); + $shopName = $shop->shop_name ?? 'My Laundry'; + $message = ""; + + if ($request->task_type == 'jemput') { + $message = "Halo *$customerName* \nDriver dari *$shopName* saat ini sedang dalam perjalanan menuju lokasi Anda untuk *MENJEMPUT* cucian. ๐Ÿ›ต๐Ÿ’จ\n\nMohon dibantu untuk menyiapkan cuciannya ya. Terima kasih! ๐Ÿ™"; + } else { + $message = "Halo *$customerName* \nDriver dari *$shopName* saat ini sedang dalam perjalanan menuju lokasi Anda untuk *MENGANTARKAN* pesanan cucian yang sudah selesai. ๐Ÿ›ต๐Ÿ’จ\n\nMohon ditunggu kedatangannya di lokasi ya. Terima kasih! ๐Ÿ™"; + } + + $success = $this->sendWhatsAppMessage($request->phone, $message); + + return response()->json(['status' => $success ? 'success' : 'error']); + } + + /** + * Helper: Konversi delivery_type ke label Indonesia + */ + private function getDeliveryTypeLabel(string $type): string + { + return match($type) { + 'pickup' => 'Pick-up', + 'delivery' => 'Delivery', + 'both' => 'Pick-up & Delivery', + default => 'Takeaway', + }; + } + + private function sendWhatsAppMessage($phoneTarget, $messageText) + { + if (empty($phoneTarget)) return false; + + $token = "2HP5kgnwLfpvShg6eBgw"; // GANTI DENGAN TOKEN FONNTE ANDA + + if (empty($token)) return false; + + try { + $response = Http::withHeaders(['Authorization' => $token])->post('https://api.fonnte.com/send', [ + 'target' => $phoneTarget, + 'message' => $messageText, + 'countryCode' => '62', + ]); + return $response->successful(); + } catch (\Exception $e) { + return false; + } + } +} \ No newline at end of file diff --git a/laundry-app/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/laundry-app/app/Http/Controllers/Auth/AuthenticatedSessionController.php new file mode 100644 index 0000000..613bcd9 --- /dev/null +++ b/laundry-app/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -0,0 +1,47 @@ +authenticate(); + + $request->session()->regenerate(); + + return redirect()->intended(route('dashboard', absolute: false)); + } + + /** + * Destroy an authenticated session. + */ + public function destroy(Request $request): RedirectResponse + { + Auth::guard('web')->logout(); + + $request->session()->invalidate(); + + $request->session()->regenerateToken(); + + return redirect('/'); + } +} diff --git a/laundry-app/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/laundry-app/app/Http/Controllers/Auth/ConfirmablePasswordController.php new file mode 100644 index 0000000..712394a --- /dev/null +++ b/laundry-app/app/Http/Controllers/Auth/ConfirmablePasswordController.php @@ -0,0 +1,40 @@ +validate([ + 'email' => $request->user()->email, + 'password' => $request->password, + ])) { + throw ValidationException::withMessages([ + 'password' => __('auth.password'), + ]); + } + + $request->session()->put('auth.password_confirmed_at', time()); + + return redirect()->intended(route('dashboard', absolute: false)); + } +} diff --git a/laundry-app/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/laundry-app/app/Http/Controllers/Auth/EmailVerificationNotificationController.php new file mode 100644 index 0000000..f64fa9b --- /dev/null +++ b/laundry-app/app/Http/Controllers/Auth/EmailVerificationNotificationController.php @@ -0,0 +1,24 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(route('dashboard', absolute: false)); + } + + $request->user()->sendEmailVerificationNotification(); + + return back()->with('status', 'verification-link-sent'); + } +} diff --git a/laundry-app/app/Http/Controllers/Auth/EmailVerificationPromptController.php b/laundry-app/app/Http/Controllers/Auth/EmailVerificationPromptController.php new file mode 100644 index 0000000..ee3cb6f --- /dev/null +++ b/laundry-app/app/Http/Controllers/Auth/EmailVerificationPromptController.php @@ -0,0 +1,21 @@ +user()->hasVerifiedEmail() + ? redirect()->intended(route('dashboard', absolute: false)) + : view('auth.verify-email'); + } +} diff --git a/laundry-app/app/Http/Controllers/Auth/NewPasswordController.php b/laundry-app/app/Http/Controllers/Auth/NewPasswordController.php new file mode 100644 index 0000000..e8368bd --- /dev/null +++ b/laundry-app/app/Http/Controllers/Auth/NewPasswordController.php @@ -0,0 +1,62 @@ + $request]); + } + + /** + * Handle an incoming new password request. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function store(Request $request): RedirectResponse + { + $request->validate([ + 'token' => ['required'], + 'email' => ['required', 'email'], + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $status = Password::reset( + $request->only('email', 'password', 'password_confirmation', 'token'), + function (User $user) use ($request) { + $user->forceFill([ + 'password' => Hash::make($request->password), + 'remember_token' => Str::random(60), + ])->save(); + + event(new PasswordReset($user)); + } + ); + + // If the password was successfully reset, we will redirect the user back to + // the application's home authenticated view. If there is an error we can + // redirect them back to where they came from with their error message. + return $status == Password::PASSWORD_RESET + ? redirect()->route('login')->with('status', __($status)) + : back()->withInput($request->only('email')) + ->withErrors(['email' => __($status)]); + } +} diff --git a/laundry-app/app/Http/Controllers/Auth/PasswordController.php b/laundry-app/app/Http/Controllers/Auth/PasswordController.php new file mode 100644 index 0000000..6916409 --- /dev/null +++ b/laundry-app/app/Http/Controllers/Auth/PasswordController.php @@ -0,0 +1,29 @@ +validateWithBag('updatePassword', [ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', Password::defaults(), 'confirmed'], + ]); + + $request->user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + return back()->with('status', 'password-updated'); + } +} diff --git a/laundry-app/app/Http/Controllers/Auth/PasswordResetLinkController.php b/laundry-app/app/Http/Controllers/Auth/PasswordResetLinkController.php new file mode 100644 index 0000000..bf1ebfa --- /dev/null +++ b/laundry-app/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -0,0 +1,44 @@ +validate([ + 'email' => ['required', 'email'], + ]); + + // We will send the password reset link to this user. Once we have attempted + // to send the link, we will examine the response then see the message we + // need to show to the user. Finally, we'll send out a proper response. + $status = Password::sendResetLink( + $request->only('email') + ); + + return $status == Password::RESET_LINK_SENT + ? back()->with('status', __($status)) + : back()->withInput($request->only('email')) + ->withErrors(['email' => __($status)]); + } +} diff --git a/laundry-app/app/Http/Controllers/Auth/RegisteredUserController.php b/laundry-app/app/Http/Controllers/Auth/RegisteredUserController.php new file mode 100644 index 0000000..d54dcd8 --- /dev/null +++ b/laundry-app/app/Http/Controllers/Auth/RegisteredUserController.php @@ -0,0 +1,51 @@ +validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class], + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + $user = User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + 'role' => 'owner', // PAKSA JADI OWNER DI SINI + ]); + + event(new Registered($user)); + + Auth::login($user); + + return redirect(route('dashboard', absolute: false)); + } +} diff --git a/laundry-app/app/Http/Controllers/Auth/VerifyEmailController.php b/laundry-app/app/Http/Controllers/Auth/VerifyEmailController.php new file mode 100644 index 0000000..784765e --- /dev/null +++ b/laundry-app/app/Http/Controllers/Auth/VerifyEmailController.php @@ -0,0 +1,27 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + } + + if ($request->user()->markEmailAsVerified()) { + event(new Verified($request->user())); + } + + return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + } +} diff --git a/laundry-app/app/Http/Controllers/Controller.php b/laundry-app/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/laundry-app/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ += 0; $i--) { + $date = now()->subDays($i); + $days[] = $date->translatedFormat('D d M'); // Contoh: Sen 12 Mei + + // Hitung jumlah order pada tanggal tersebut + $chartData[] = \App\Models\Order::whereDate('created_at', $date->toDateString())->count(); + } + + // Mencari nilai tertinggi untuk skala grafik (minimal 10 agar tidak mentok ke atas) + $maxVal = max($chartData) > 0 ? max($chartData) : 10; + + // Konversi nilai data ke koordinat SVG (Tinggi SVG kita 200) + // Rumus: 200 - (nilai / maxVal * 150) -> 150 agar ada sisa ruang di atas + $points = ""; + foreach ($chartData as $index => $value) { + $x = $index * 166.6; + $y = 200 - ($value / $maxVal * 150); + $points .= "$x,$y "; + } + + return view('dashboard', [ + 'total' => \App\Models\Order::whereNotIn('status', ['DIAMBIL', 'DITERIMA'])->count(), + 'antrian' => \App\Models\Order::where('status', 'ANTRIAN')->count(), + 'proses' => \App\Models\Order::where('status', 'PROSES')->count(), + 'selesai' => \App\Models\Order::where('status', 'SELESAI')->count(), + 'latest' => \App\Models\Order::latest()->take(5)->get(), + 'chartPoints' => trim($points), + 'days' => $days, + 'chartData' => $chartData + ]); + } +} diff --git a/laundry-app/app/Http/Controllers/LaundryController.php b/laundry-app/app/Http/Controllers/LaundryController.php new file mode 100644 index 0000000..210ac3b --- /dev/null +++ b/laundry-app/app/Http/Controllers/LaundryController.php @@ -0,0 +1,101 @@ +validate([ + 'name' => 'required|string|max:255', + 'price' => 'required|numeric', + 'unit' => 'required|string', + ]); + + Service::create([ + 'name' => $request->name, + 'price' => $request->price, + 'unit' => $request->unit, + 'is_active' => true, + ]); + + return redirect()->route('laundry')->with('success', 'Layanan berhasil ditambahkan!'); + } + + /** + * --- FUNGSI EDIT (YANG TADI HILANG) --- + * Menampilkan halaman form edit layanan + */ + public function edit($id) + { + $service = Service::findOrFail($id); + return view('laundry.edit', compact('service')); + } + + /** + * --- FUNGSI UPDATE (UNTUK MENYIMPAN PERUBAHAN) --- + */ + public function update(Request $request, $id) + { + $request->validate([ + 'name' => 'required|string|max:255', + 'price' => 'required|numeric', + 'unit' => 'required|string', + ]); + + $service = Service::findOrFail($id); + $service->update([ + 'name' => $request->name, + 'price' => $request->price, + 'unit' => $request->unit, + ]); + + return redirect()->route('laundry')->with('success', 'Layanan berhasil diperbarui!'); + } + + /** + * Mengubah status aktif/non-aktif (Toggle) + */ + public function updateStatus($id) + { + $service = Service::findOrFail($id); + $service->is_active = !$service->is_active; + $service->save(); + + return back()->with('success', 'Status layanan berhasil diperbarui!'); + } + + /** + * Menghapus layanan + */ + public function destroy($id) + { + $service = Service::findOrFail($id); + $service->delete(); + + return back()->with('success', 'Layanan berhasil dihapus!'); + } +} \ No newline at end of file diff --git a/laundry-app/app/Http/Controllers/OrderListController.php b/laundry-app/app/Http/Controllers/OrderListController.php new file mode 100644 index 0000000..3b3cf34 --- /dev/null +++ b/laundry-app/app/Http/Controllers/OrderListController.php @@ -0,0 +1,57 @@ +where('status', $statusUpper); + + // 3. Logika Pencarian (Jika ada input 'search') + if ($request->has('search') && $request->search != '') { + $searchTerm = $request->search; + $query->where(function($q) use ($searchTerm) { + $q->where('customer_name', 'like', '%' . $searchTerm . '%') + ->orWhere('order_number', 'like', '%' . $searchTerm . '%'); + }); + } + + // 4. Ambil data dengan Pagination & urutan terbaru + // appends() digunakan agar saat pindah halaman, hasil search tidak hilang + $orders = $query->latest() + ->paginate(10) + ->appends(['search' => $request->search]); + + // 5. Kirim ke view + return view('orders.status_list', compact('orders', 'statusUpper')); + } + + public function bulkDelete(Request $request) + { + $ids = is_array($request->selected_ids) ? array_unique($request->selected_ids) : []; + + if (empty($ids)) { + return redirect()->back()->with('error', 'Pilih minimal satu pesanan untuk dihapus.'); + } + + try { + Order::whereIn('id', $ids)->delete(); + return redirect()->back()->with('success', count($ids) . ' pesanan berhasil dihapus.'); + } catch (\Exception $e) { + return redirect()->back()->with('error', 'Terjadi kesalahan saat menghapus data.'); + } + } +} \ No newline at end of file diff --git a/laundry-app/app/Http/Controllers/ProfileController.php b/laundry-app/app/Http/Controllers/ProfileController.php new file mode 100644 index 0000000..a48eb8d --- /dev/null +++ b/laundry-app/app/Http/Controllers/ProfileController.php @@ -0,0 +1,60 @@ + $request->user(), + ]); + } + + /** + * Update the user's profile information. + */ + public function update(ProfileUpdateRequest $request): RedirectResponse + { + $request->user()->fill($request->validated()); + + if ($request->user()->isDirty('email')) { + $request->user()->email_verified_at = null; + } + + $request->user()->save(); + + return Redirect::route('profile.edit')->with('status', 'profile-updated'); + } + + /** + * Delete the user's account. + */ + public function destroy(Request $request): RedirectResponse + { + $request->validateWithBag('userDeletion', [ + 'password' => ['required', 'current_password'], + ]); + + $user = $request->user(); + + Auth::logout(); + + $user->delete(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return Redirect::to('/'); + } +} diff --git a/laundry-app/app/Http/Controllers/ReportController.php b/laundry-app/app/Http/Controllers/ReportController.php new file mode 100644 index 0000000..f6b9fd5 --- /dev/null +++ b/laundry-app/app/Http/Controllers/ReportController.php @@ -0,0 +1,140 @@ +get('start_date', Carbon::now()->startOfMonth()->format('Y-m-d')); + $endDate = $request->get('end_date', Carbon::now()->format('Y-m-d')); + $deliveryType = $request->get('delivery_type', 'all'); + $paymentMethod = $request->get('payment_method', 'all'); + + // Filter data yang statusnya selesai ('DIAMBIL' atau 'DITERIMA') + $query = Order::with('items') + ->whereIn('status', ['DIAMBIL', 'DITERIMA']) + ->whereBetween('updated_at', [$startDate . ' 00:00:00', $endDate . ' 23:59:59']); + + if ($deliveryType !== 'all') { + if ($deliveryType === 'none') { + $query->where(function($q) { + $q->where('delivery_type', 'none')->orWhereNull('delivery_type'); + }); + } else { + $query->where('delivery_type', $deliveryType); + } + } + + if ($paymentMethod !== 'all') { + $query->where('payment_method', $paymentMethod); + } + + $reports = $query->orderBy('updated_at', 'desc')->get(); + $totalRevenue = $query->sum('total_price'); + $totalDeliveryFee = $query->sum('delivery_fee'); + $totalLaundryRevenue = $totalRevenue - $totalDeliveryFee; + $totalOrders = $query->count(); + + return view('reports', compact('reports', 'totalRevenue', 'totalDeliveryFee', 'totalLaundryRevenue', 'totalOrders', 'startDate', 'endDate', 'deliveryType', 'paymentMethod')); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'customer_name' => 'required', + 'phone' => 'required', + 'service_name' => 'required', + 'total_price' => 'required|integer', + 'payment_method' => 'nullable|string', // Pastikan App mengirim ini + 'cash_received' => 'nullable|integer', // Pastikan App mengirim ini + ]); + + // Hitung kembalian + $total = $validated['total_price']; + $bayar = $request->cash_received ?? 0; + $kembali = ($bayar > $total) ? ($bayar - $total) : 0; + + $ownerId = auth()->check() ? (auth()->user()->getOwnerId() ?? auth()->id()) : null; + $orderCount = $ownerId ? Order::where('user_id', $ownerId)->count() : Order::count(); + $prefix = $ownerId ? 'ORD-' . date('Ymd') . '-' . $ownerId . '-' : 'ORD-' . date('Ymd') . '-'; + + $order = Order::create([ + 'order_number' => $prefix . str_pad($orderCount + 1, 3, '0', STR_PAD_LEFT), + 'user_id' => $ownerId, + 'customer_name' => $validated['customer_name'], + 'phone' => $validated['phone'], + 'service_name' => $validated['service_name'], + 'total_price' => $total, + 'status' => 'ANTRIAN', + 'payment_method' => strtoupper($request->payment_method ?? 'CASH'), + 'cash_received' => $bayar, + 'cash_change' => $kembali, + ]); + + + return response()->json(['status' => 'success', 'data' => $order]); + } + + public function export(Request $request) + { + $startDate = $request->get('start_date', now()->startOfMonth()->format('Y-m-d')); + $endDate = $request->get('end_date', now()->format('Y-m-d')); + $deliveryType = $request->get('delivery_type', 'all'); + $paymentMethod = $request->get('payment_method', 'all'); + + $paymentLabel = $paymentMethod !== 'all' ? '-' . strtoupper($paymentMethod) : ''; + if ($paymentLabel === '-CASH') $paymentLabel = '-TUNAI'; + + return Excel::download(new ReportExport($startDate, $endDate, $deliveryType, $paymentMethod), "laporan-laundry{$paymentLabel}-{$startDate}-to-{$endDate}.xlsx"); + } + + public function exportBulk(Request $request) + { + $request->validate([ + 'ids' => 'required|array', + 'ids.*' => 'exists:orders,id' + ]); + + $startDate = $request->get('start_date', now()->startOfMonth()->format('Y-m-d')); + $endDate = $request->get('end_date', now()->format('Y-m-d')); + $deliveryType = $request->get('delivery_type', 'all'); + $paymentMethod = $request->get('payment_method', 'all'); + + $paymentLabel = $paymentMethod !== 'all' ? '-' . strtoupper($paymentMethod) : ''; + if ($paymentLabel === '-CASH') $paymentLabel = '-TUNAI'; + + return Excel::download(new ReportExport($startDate, $endDate, $deliveryType, $paymentMethod, $request->ids), "laporan-laundry-terpilih{$paymentLabel}.xlsx"); + } + + // --- FUNGSI BARU UNTUK HAPUS BANYAK DATA SEKALIGUS --- + public function bulkDelete(Request $request) + { + // 1. Validasi apakah ada array 'ids' yang dikirim dari checkbox + $request->validate([ + 'ids' => 'required|array', + 'ids.*' => 'exists:orders,id' + ]); + + try { + // Ambil ID unik karena tampilan mobile & desktop mengirimkan ID ganda + $uniqueIds = is_array($request->ids) ? array_unique($request->ids) : []; + + // 2. Eksekusi penghapusan data + Order::whereIn('id', $uniqueIds)->delete(); + + // 3. Kembalikan ke halaman sebelumnya dengan pesan sukses + return redirect()->back()->with('success', count($uniqueIds) . ' data laporan berhasil dihapus.'); + } catch (\Exception $e) { + // Jika terjadi error (misalnya masalah database) + return redirect()->back()->with('error', 'Gagal menghapus data: ' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/laundry-app/app/Http/Controllers/StaffController.php b/laundry-app/app/Http/Controllers/StaffController.php new file mode 100644 index 0000000..056e609 --- /dev/null +++ b/laundry-app/app/Http/Controllers/StaffController.php @@ -0,0 +1,84 @@ +validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users', + 'password' => 'required|min:8', + 'role' => 'required|in:staf,driver', + ]); + + // 2. Simpan ke database + \App\Models\User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => \Illuminate\Support\Facades\Hash::make($request->password), + 'role' => $request->role, + // TAMBAHKAN BARIS DI BAWAH INI: + 'owner_id' => Auth::id(), + ]); + + // 3. Arahkan kembali + return redirect()->route('settings')->with('success', 'Akun ' . $request->role . ' berhasil ditambahkan!'); + } + + public function edit($id) + { + $staff = \App\Models\User::where('owner_id', Auth::id())->findOrFail($id); + return view('staff.edit', compact('staff')); + } + + public function update(Request $request, $id) + { + $staff = \App\Models\User::where('owner_id', Auth::id())->findOrFail($id); + + $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users,email,' . $staff->id, + 'password' => 'nullable|min:8', + 'role' => 'required|in:staf,driver', + ]); + + $staff->name = $request->name; + $staff->email = $request->email; + $staff->role = $request->role; + + if ($request->filled('password')) { + $staff->password = \Illuminate\Support\Facades\Hash::make($request->password); + } + + $staff->save(); + + return redirect()->route('settings')->with('success', 'Akun berhasil diperbarui!'); + } + + public function destroy($id) + { + // Tambahkan filter owner_id agar Owner A tidak bisa menghapus staf Owner B lewat URL + $staff = \App\Models\User::where('owner_id', Auth::id())->findOrFail($id); + + if ($staff->id === Auth::id()) { + return redirect()->back()->with('error', 'Anda tidak bisa menghapus akun sendiri!'); + } + + $staff->delete(); + + return redirect()->route('settings')->with('success', 'Akun berhasil dihapus.'); + } +} diff --git a/laundry-app/app/Http/Controllers/UserController.php b/laundry-app/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..d4c8a29 --- /dev/null +++ b/laundry-app/app/Http/Controllers/UserController.php @@ -0,0 +1,75 @@ + $ownerId], + [ + 'shop_name' => '', + 'delivery_fee_per_km' => 0, + 'free_delivery_km' => 0, + 'is_delivery_active' => false, + 'is_estimation_active' => false, + 'express_extra_price' => 0, + 'kilat_extra_price' => 0, + ] + ); + + $staffs = User::where('owner_id', $ownerId)->get(); + + return view('settings', compact('staffs', 'shop')); + } + + public function updateShopSettings(Request $request) + { + $request->validate([ + 'shop_name' => 'nullable|string|max:255', + 'delivery_fee_per_km' => 'required_if:is_delivery_active,1|numeric|min:1', + 'free_delivery_km' => 'nullable|numeric|min:0', + 'is_delivery_active' => 'nullable', + 'is_estimation_active' => 'nullable', + 'express_extra_price' => 'required_if:is_estimation_active,1|numeric|min:1', + 'kilat_extra_price' => 'required_if:is_estimation_active,1|numeric|min:1', + 'latitude' => 'nullable|numeric', + 'longitude' => 'nullable|numeric', + ], [ + 'delivery_fee_per_km.required_if' => 'Tarif per KM harus diisi jika Ongkos Kirim diaktifkan.', + 'delivery_fee_per_km.min' => 'Tarif per KM harus lebih dari 0.', + 'express_extra_price.required_if' => 'Harga Express harus diisi jika Estimasi diaktifkan.', + 'express_extra_price.min' => 'Harga Express harus lebih dari 0.', + 'kilat_extra_price.required_if' => 'Harga Kilat harus diisi jika Estimasi diaktifkan.', + 'kilat_extra_price.min' => 'Harga Kilat harus lebih dari 0.', + ]); + + $shop = Shop::where('user_id', Auth::id())->first(); + if ($shop) { + $dataToUpdate = [ + 'shop_name' => $request->shop_name, + 'delivery_fee_per_km' => $request->delivery_fee_per_km ?? 0, + 'free_delivery_km' => $request->free_delivery_km ?? 0, + 'is_delivery_active' => $request->has('is_delivery_active'), + 'is_estimation_active' => $request->has('is_estimation_active'), + 'express_extra_price' => $request->express_extra_price ?? 0, + 'kilat_extra_price' => $request->kilat_extra_price ?? 0, + 'latitude' => $request->latitude, + 'longitude' => $request->longitude, + ]; + + $shop->update($dataToUpdate); + } + + return redirect()->back()->with('success', 'Pengaturan Toko berhasil diperbarui!'); + } +} diff --git a/laundry-app/app/Http/Middleware/OnlyOwner.php b/laundry-app/app/Http/Middleware/OnlyOwner.php new file mode 100644 index 0000000..31bc488 --- /dev/null +++ b/laundry-app/app/Http/Middleware/OnlyOwner.php @@ -0,0 +1,30 @@ +role !== 'owner') { + Auth::logout(); + + // Hancurkan session agar benar-benar bersih + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect('/login')->with('error', 'Akses Ditolak! Hanya Owner yang boleh masuk.'); + } + + return $next($request); + } +} \ No newline at end of file diff --git a/laundry-app/app/Http/Middleware/PreventBackHistory.php b/laundry-app/app/Http/Middleware/PreventBackHistory.php new file mode 100644 index 0000000..bbbc236 --- /dev/null +++ b/laundry-app/app/Http/Middleware/PreventBackHistory.php @@ -0,0 +1,25 @@ +headers->set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0'); + $response->headers->set('Pragma', 'no-cache'); + $response->headers->set('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT'); + + return $response; + } +} diff --git a/laundry-app/app/Http/Requests/Auth/LoginRequest.php b/laundry-app/app/Http/Requests/Auth/LoginRequest.php new file mode 100644 index 0000000..8a97209 --- /dev/null +++ b/laundry-app/app/Http/Requests/Auth/LoginRequest.php @@ -0,0 +1,88 @@ +|string> + */ + public function rules(): array + { + return [ + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string'], + ]; + } + + /** + * Attempt to authenticate the request's credentials. + * + * @throws \Illuminate\Validation\ValidationException + */ + // app/Http/Requests/Auth/LoginRequest.php + + public function authenticate(): void + { + $this->ensureIsNotRateLimited(); + + // Baris ini yang bertugas mengecek checkbox 'remember' + if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.failed'), + ]); + } + + RateLimiter::clear($this->throttleKey()); + } + + /** + * Ensure the login request is not rate limited. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function ensureIsNotRateLimited(): void + { + if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + return; + } + + event(new Lockout($this)); + + $seconds = RateLimiter::availableIn($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ]), + ]); + } + + /** + * Get the rate limiting throttle key for the request. + */ + public function throttleKey(): string + { + return Str::transliterate(Str::lower($this->string('email')) . '|' . $this->ip()); + } +} diff --git a/laundry-app/app/Http/Requests/ProfileUpdateRequest.php b/laundry-app/app/Http/Requests/ProfileUpdateRequest.php new file mode 100644 index 0000000..3622a8f --- /dev/null +++ b/laundry-app/app/Http/Requests/ProfileUpdateRequest.php @@ -0,0 +1,30 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'lowercase', + 'email', + 'max:255', + Rule::unique(User::class)->ignore($this->user()->id), + ], + ]; + } +} diff --git a/laundry-app/app/Livewire/ActiveOrders.php b/laundry-app/app/Livewire/ActiveOrders.php new file mode 100644 index 0000000..c3cdb6c --- /dev/null +++ b/laundry-app/app/Livewire/ActiveOrders.php @@ -0,0 +1,22 @@ + Order::with('items') + ->whereNotIn('status', ['DIAMBIL', 'DITERIMA']) + ->latest() + ->paginate(10), + ]); + } +} diff --git a/laundry-app/app/Livewire/DashboardStats.php b/laundry-app/app/Livewire/DashboardStats.php new file mode 100644 index 0000000..a5d3d92 --- /dev/null +++ b/laundry-app/app/Livewire/DashboardStats.php @@ -0,0 +1,19 @@ + Order::whereNotIn('status', ['DIAMBIL', 'DITERIMA'])->count(), + 'antrian' => Order::where('status', 'ANTRIAN')->count(), + 'proses' => Order::where('status', 'PROSES')->count(), + 'selesai' => Order::where('status', 'SELESAI')->count(), + ]); + } +} diff --git a/laundry-app/app/Models/Order.php b/laundry-app/app/Models/Order.php new file mode 100644 index 0000000..c10533f --- /dev/null +++ b/laundry-app/app/Models/Order.php @@ -0,0 +1,85 @@ + 'datetime', + 'total_price' => 'integer', + 'total_weight' => 'float', + ]; + + public function items() + { + return $this->hasMany(OrderItem::class); + } + + /** + * Accessor untuk mendapatkan service_name dari items + * Menghindari error di view karena kolom service_name dihapus dari tabel orders + */ + public function getServiceNameAttribute() + { + if ($this->items->isEmpty()) { + return 'Layanan Umum'; + } + + return $this->items->map(function ($item) { + $qty = $item->unit === 'PCS' ? round($item->qty_or_weight) . ' PCS' : number_format($item->qty_or_weight, 3, '.', '') . ' KG'; + return $item->service_name . ' (' . $qty . ')||' . $item->subtotal; + })->implode("\n"); + } + + /** + * Logic Booted untuk Multi-Tenancy + */ + protected static function booted() + { + static::addGlobalScope('owner', function (Builder $builder) { + if (Auth::check()) { + $user = Auth::user(); + $ownerId = $user->getOwnerId(); + $builder->where('user_id', $ownerId); + } + }); + + static::creating(function ($order) { + if (Auth::check()) { + $user = Auth::user(); + $order->user_id = $user->getOwnerId(); + } + }); + } +} \ No newline at end of file diff --git a/laundry-app/app/Models/OrderItem.php b/laundry-app/app/Models/OrderItem.php new file mode 100644 index 0000000..1a3d87a --- /dev/null +++ b/laundry-app/app/Models/OrderItem.php @@ -0,0 +1,38 @@ + 'integer', + 'qty_or_weight' => 'float', + 'subtotal' => 'integer' + ]; + + public function order() + { + return $this->belongsTo(Order::class); + } + + public function service() + { + return $this->belongsTo(Service::class); + } +} diff --git a/laundry-app/app/Models/Service.php b/laundry-app/app/Models/Service.php new file mode 100644 index 0000000..e1400a9 --- /dev/null +++ b/laundry-app/app/Models/Service.php @@ -0,0 +1,51 @@ +hasMany(Order::class); + } + + /** + * Logic Booted untuk Multi-Tenancy + */ + protected static function booted() + { + static::addGlobalScope('owner', function (Builder $builder) { + if (Auth::check()) { + $user = Auth::user(); + $ownerId = $user->getOwnerId(); + $builder->where('user_id', $ownerId); + } + }); + + static::creating(function ($service) { + if (Auth::check()) { + $user = Auth::user(); + $service->user_id = $user->getOwnerId(); + } + }); + } +} diff --git a/laundry-app/app/Models/Shop.php b/laundry-app/app/Models/Shop.php new file mode 100644 index 0000000..5080979 --- /dev/null +++ b/laundry-app/app/Models/Shop.php @@ -0,0 +1,44 @@ + 'boolean', + 'is_estimation_active' => 'boolean', + 'free_delivery_km' => 'integer', + 'express_extra_price' => 'integer', + 'kilat_extra_price' => 'integer', + ]; + + /** + * Relasi: Shop milik seorang Owner + */ + public function owner(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } +} diff --git a/laundry-app/app/Models/User.php b/laundry-app/app/Models/User.php new file mode 100644 index 0000000..6b3b7d5 --- /dev/null +++ b/laundry-app/app/Models/User.php @@ -0,0 +1,89 @@ + 'datetime', + 'password' => 'hashed', + ]; + } + + /** + * Relasi: Seorang Owner punya banyak Staff/Driver + */ + public function staffs(): HasMany + { + return $this->hasMany(User::class, 'owner_id'); + } + + /** + * Relasi: Seorang Staff/Driver punya satu Owner (Bos) + */ + public function owner(): BelongsTo + { + return $this->belongsTo(User::class, 'owner_id'); + } + + /** + * Relasi: Seorang Owner punya satu Toko + */ + public function shop(): HasOne + { + return $this->hasOne(Shop::class, 'user_id'); + } + + /** + * Helper: Dapatkan Owner ID (baik user ini owner atau staf/driver) + */ + public function getOwnerId(): int + { + return ($this->role === 'staf' || $this->role === 'driver') ? $this->owner_id : $this->id; + } + + /** + * Helper: Dapatkan data Shop milik Owner + */ + public function getOwnerShop(): ?Shop + { + $ownerId = $this->getOwnerId(); + return Shop::where('user_id', $ownerId)->first(); + } + + /** + * Send the password reset notification. + * + * @param string $token + * @return void + */ + public function sendPasswordResetNotification($token) + { + $this->notify(new \App\Notifications\CustomResetPassword($token)); + } +} diff --git a/laundry-app/app/Notifications/CustomResetPassword.php b/laundry-app/app/Notifications/CustomResetPassword.php new file mode 100644 index 0000000..b6f759a --- /dev/null +++ b/laundry-app/app/Notifications/CustomResetPassword.php @@ -0,0 +1,106 @@ +sendViaBrevoApi($notifiable, $apiKey); + // Kembalikan array kosong agar Laravel tidak memproses channel mail + return []; + } + + // Fallback: gunakan mail channel SMTP biasa (untuk lokal) + return ['mail']; + } + + /** + * Kirim email reset sandi menggunakan Brevo HTTP API. + * Menggunakan HTTP (bukan SMTP) sehingga tidak terblokir di Railway. + */ + protected function sendViaBrevoApi($notifiable, string $apiKey): void + { + $url = $this->resetUrl($notifiable); + $expire = config('auth.passwords.' . config('auth.defaults.passwords') . '.expire', 60); + + $htmlContent = " +
+

Permintaan Reset Kata Sandi

+

Halo, " . ($notifiable->name ?? '') . "!

+

Anda menerima email ini karena kami menerima permintaan untuk menyetel ulang kata sandi akun Anda di mylaundry.

+

+ + Reset Kata Sandi + +

+

Tautan ini akan kedaluwarsa dalam {$expire} menit.

+

Jika Anda tidak meminta reset kata sandi, abaikan email ini.

+
+

Salam hangat, mylaundry

+
+ "; + + try { + $response = Http::timeout(10)->withHeaders([ + 'api-key' => $apiKey, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ])->post('https://api.brevo.com/v3/smtp/email', [ + 'sender' => [ + 'name' => config('mail.from.name', 'mylaundry'), + 'email' => config('mail.from.address'), + ], + 'to' => [ + ['email' => $notifiable->email, 'name' => $notifiable->name ?? ''], + ], + 'subject' => 'Permintaan Reset Kata Sandi - mylaundry', + 'htmlContent' => $htmlContent, + ]); + + if ($response->successful()) { + Log::info('Brevo API: Email reset sandi berhasil dikirim ke ' . $notifiable->email); + } else { + Log::error('Brevo API Error [' . $response->status() . ']: ' . $response->body()); + } + } catch (\Exception $e) { + Log::error('Brevo API Exception: ' . $e->getMessage()); + } + } + + /** + * Build the mail representation (digunakan sebagai fallback SMTP lokal). + */ + public function toMail($notifiable): MailMessage + { + if (static::$toMailCallback) { + return call_user_func(static::$toMailCallback, $notifiable, $this->token); + } + + $url = $this->resetUrl($notifiable); + $expire = config('auth.passwords.' . config('auth.defaults.passwords') . '.expire', 60); + + return (new MailMessage) + ->subject('Permintaan Reset Kata Sandi - mylaundry') + ->greeting('Halo!') + ->line('Anda menerima email ini karena kami menerima permintaan untuk menyetel ulang kata sandi akun Anda.') + ->action('Reset Kata Sandi', $url) + ->line("Tautan reset kata sandi ini akan kedaluwarsa dalam waktu {$expire} menit.") + ->line('Jika Anda tidak meminta reset kata sandi, abaikan email ini.') + ->salutation('Salam hangat, mylaundry'); + } +} diff --git a/laundry-app/app/Providers/AppServiceProvider.php b/laundry-app/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..8ee7e32 --- /dev/null +++ b/laundry-app/app/Providers/AppServiceProvider.php @@ -0,0 +1,31 @@ +handleCommand(new ArgvInput); + +exit($status); diff --git a/laundry-app/bootstrap/app.php b/laundry-app/bootstrap/app.php new file mode 100644 index 0000000..3533a13 --- /dev/null +++ b/laundry-app/bootstrap/app.php @@ -0,0 +1,26 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware) { + // Trust all proxies when behind load balancers like Railway + $middleware->trustProxies(at: '*'); + + // Daftarkan alias middleware Anda di sini + $middleware->alias([ + 'owner' => \App\Http\Middleware\OnlyOwner::class, + 'prevent-back-history' => \App\Http\Middleware\PreventBackHistory::class, + ]); + }) + ->withExceptions(function (Exceptions $exceptions) { + // + })->create(); \ No newline at end of file diff --git a/laundry-app/bootstrap/cache/.gitignore b/laundry-app/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/laundry-app/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/laundry-app/bootstrap/providers.php b/laundry-app/bootstrap/providers.php new file mode 100644 index 0000000..38b258d --- /dev/null +++ b/laundry-app/bootstrap/providers.php @@ -0,0 +1,5 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.19.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", + "shasum": "" + }, + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" + }, + "time": "2025-10-17T16:34:55+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v7.0.5", + "source": { + "type": "git", + "url": "https://github.com/googleapis/php-jwt.git", + "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380", + "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpfastcache/phpfastcache": "^9.2", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/googleapis/php-jwt/issues", + "source": "https://github.com/googleapis/php-jwt/tree/v7.0.5" + }, + "time": "2026-04-01T20:38:03+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "47ba23c7a55247e2e1b7407aca90e9bbed0d9d86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/47ba23c7a55247e2e1b7407aca90e9bbed0d9d86", + "reference": "47ba23c7a55247e2e1b7407aca90e9bbed0d9d86", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "guzzlehttp/test-server": "^0.3.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Mรกrk Sรกgi-Kazรกr", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2026-05-20T22:59:19+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/09e8a212562fb1fb6a512c4156ed71525969d6c2", + "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.4.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2026-05-20T22:57:30+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.10.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "73ab136360b5dfd858006eae9795e8fe43c80361" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/73ab136360b5dfd858006eae9795e8fe43c80361", + "reference": "73ab136360b5dfd858006eae9795e8fe43c80361", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Mรกrk Sรกgi-Kazรกr", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Mรกrk Sรกgi-Kazรกr", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.10.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-05-20T09:27:36+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.60.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "b8b55ce32175cc00f834a56eeb6316f18ed6ea39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/b8b55ce32175cc00f834a56eeb6316f18ed6ea39", + "reference": "b8b55ce32175cc00f834a56eeb6316f18ed6ea39", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.34", + "symfony/polyfill-php85": "^1.34", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.9.0", + "pda/pheanstalk": "^5.0.6|^7.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.1.41", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", + "resend/resend-php": "^0.10.0|^1.0", + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-05-20T11:48:19+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.18", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "a19af51bb144bf87f08397921fa619f85c7d4e72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/a19af51bb144bf87f08397921fa619f85c7d4e72", + "reference": "a19af51bb144bf87f08397921fa619f85c7d4e72", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.18" + }, + "time": "2026-05-19T00:47:18+00:00" + }, + { + "name": "laravel/sanctum", + "version": "v4.3.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "2a9bccc18e9907808e0018dd15fa643937886b1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/2a9bccc18e9907808e0018dd15fa643937886b1e", + "reference": "2a9bccc18e9907808e0018dd15fa643937886b1e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2026-04-30T11:46:25+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.13", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-04-16T14:03:50+00:00" + }, + { + "name": "laravel/socialite", + "version": "v5.27.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "40e0757a75637c7b2dff05d3286b0d8fc25e5c0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/40e0757a75637c7b2dff05d3286b0d8fc25e5c0e", + "reference": "40e0757a75637c7b2dff05d3286b0d8fc25e5c0e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^6.4|^7.0", + "guzzlehttp/guzzle": "^6.0|^7.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "league/oauth1-client": "^1.11", + "php": "^7.2|^8.0", + "phpseclib/phpseclib": "^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.18|^5.20|^6.47|^7.55|^8.36|^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.12.23", + "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5|^12.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Socialite": "Laravel\\Socialite\\Facades\\Socialite" + }, + "providers": [ + "Laravel\\Socialite\\SocialiteServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", + "homepage": "https://laravel.com", + "keywords": [ + "laravel", + "oauth" + ], + "support": { + "issues": "https://github.com/laravel/socialite/issues", + "source": "https://github.com/laravel/socialite" + }, + "time": "2026-04-24T14:05:47+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.11.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/c9f80cc835649b5c1842898fb043f8cc098dd741", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741", + "shasum": "" + }, + "require": { + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^7.2.5|^8.0", + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v2.11.1" + }, + "time": "2026-02-06T14:12:35+00:00" + }, + { + "name": "league/commonmark", + "version": "2.8.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-03-19T13:16:38+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.34.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e", + "reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.34.0" + }, + "time": "2026-05-14T10:28:08+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + }, + "time": "2026-01-23T15:30:45+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/oauth1-client", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth1-client.git", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "guzzlehttp/psr7": "^1.7|^2.0", + "php": ">=7.1||>=8.0" + }, + "require-dev": { + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^2.17", + "mockery/mockery": "^1.3.3", + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5||9.5" + }, + "suggest": { + "ext-simplexml": "For decoding XML-based responses." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev", + "dev-develop": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth1\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Corlett", + "email": "bencorlett@me.com", + "homepage": "http://www.webcomm.com.au", + "role": "Developer" + } + ], + "description": "OAuth 1.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "bitbucket", + "identity", + "idp", + "oauth", + "oauth1", + "single sign on", + "trello", + "tumblr", + "twitter" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth1-client/issues", + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.11.0" + }, + "time": "2024-12-10T19:59:05+00:00" + }, + { + "name": "league/uri", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "livewire/livewire", + "version": "v4.3.0", + "source": { + "type": "git", + "url": "https://github.com/livewire/livewire.git", + "reference": "19ebb1ee4d057debceccf70ff01950e6a6114edc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/livewire/livewire/zipball/19ebb1ee4d057debceccf70ff01950e6a6114edc", + "reference": "19ebb1ee4d057debceccf70ff01950e6a6114edc", + "shasum": "" + }, + "require": { + "illuminate/database": "^10.0|^11.0|^12.0|^13.0", + "illuminate/routing": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "illuminate/validation": "^10.0|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.24|^0.2|^0.3", + "league/mime-type-detection": "^1.9", + "php": "^8.1", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/http-kernel": "^6.2|^7.0|^8.0" + }, + "require-dev": { + "calebporzio/sushi": "^2.1", + "laravel/framework": "^10.15.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.3.1", + "orchestra/testbench": "^8.21.0|^9.0|^10.0|^11.0", + "orchestra/testbench-dusk": "^8.24|^9.1|^10.0|^11.0", + "phpunit/phpunit": "^10.4|^11.5|^12.5", + "psy/psysh": "^0.11.22|^0.12" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Livewire": "Livewire\\Livewire" + }, + "providers": [ + "Livewire\\LivewireServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Livewire\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Caleb Porzio", + "email": "calebporzio@gmail.com" + } + ], + "description": "A front-end framework for Laravel.", + "support": { + "issues": "https://github.com/livewire/livewire/issues", + "source": "https://github.com/livewire/livewire/tree/v4.3.0" + }, + "funding": [ + { + "url": "https://github.com/livewire", + "type": "github" + } + ], + "time": "2026-05-01T00:46:07+00:00" + }, + { + "name": "maatwebsite/excel", + "version": "3.1.69", + "source": { + "type": "git", + "url": "https://github.com/SpartnerNL/Laravel-Excel.git", + "reference": "ae5d65b7c9a2fac43bff4d44f796ac95d7a8e760" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/ae5d65b7c9a2fac43bff4d44f796ac95d7a8e760", + "reference": "ae5d65b7c9a2fac43bff4d44f796ac95d7a8e760", + "shasum": "" + }, + "require": { + "composer/semver": "^3.3", + "ext-json": "*", + "illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0||^13.0", + "php": "^7.0||^8.0", + "phpoffice/phpspreadsheet": "^1.30.4", + "psr/simple-cache": "^1.0||^2.0||^3.0" + }, + "require-dev": { + "laravel/scout": "^7.0||^8.0||^9.0||^10.0||^11.0", + "orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0||^11.0", + "predis/predis": "^1.1" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Excel": "Maatwebsite\\Excel\\Facades\\Excel" + }, + "providers": [ + "Maatwebsite\\Excel\\ExcelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Maatwebsite\\Excel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Patrick Brouwers", + "email": "patrick@spartner.nl" + } + ], + "description": "Supercharged Excel exports and imports in Laravel", + "keywords": [ + "PHPExcel", + "batch", + "csv", + "excel", + "export", + "import", + "laravel", + "php", + "phpspreadsheet" + ], + "support": { + "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues", + "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.69" + }, + "funding": [ + { + "url": "https://laravel-excel.com/commercial-support", + "type": "custom" + }, + { + "url": "https://github.com/patrickbrouwers", + "type": "github" + } + ], + "time": "2026-04-30T20:03:58+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.2" + }, + "require-dev": { + "brianium/paratest": "^7.7", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.16", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^11.0", + "vimeo/psalm": "^6.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Mรคnnchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "Andrรกs Kolesรกr", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2025-01-27T12:07:53+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.11.4", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/e890471a3494740f7d9326d72ce6a8c559ffee60", + "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-04-07T09:57:54+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "๐Ÿ“ Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.5" + }, + "time": "2026-02-23T03:47:12+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.4", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "๐Ÿ›  Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.4" + }, + "time": "2026-05-11T20:49:54+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.30.4", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "02970383cc12e7bf0bc0707ea6e2e8ed23a7aec9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/02970383cc12e7bf0bc0707ea6e2e8ed23a7aec9", + "reference": "02970383cc12e7bf0bc0707ea6e2e8ed23a7aec9", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.15", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": ">=7.4.0 <8.5.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "doctrine/instantiator": "^1.5", + "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + }, + { + "name": "Owen Leibman" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.4" + }, + "time": "2026-04-19T06:00:39+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.52", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jรผrgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.52" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2026-04-27T07:02:15+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.22", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/3be75d5b9244936dd4ac62ade2bfb004d13acf0f", + "reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.22" + }, + "time": "2026-03-22T23:03:24+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.2" + }, + "time": "2025-12-14T04:43:48+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "ed0107e43ab452aa77ae99e005b95e56b556e075" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/ed0107e43ab452aa77ae99e005b95e56b556e075", + "reference": "ed0107e43ab452aa77ae99e005b95e56b556e075", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-13T12:04:42+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "b75663ed96cf4756e28e3105476f220f92886cc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/b75663ed96cf4756e28e3105476f220f92886cc4", + "reference": "b75663ed96cf4756e28e3105476f220f92886cc4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-Franรงois Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-18T13:18:21+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-13T15:52:40+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-18T13:18:21+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T13:30:16+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "e0be088d22278583a82da281886e8c3592fbf149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "9381209597ec66c25be154cbf2289076e64d1eab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9381209597ec66c25be154cbf2289076e64d1eab", + "reference": "9381209597ec66c25be154cbf2289076e64d1eab", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "7922b53e70d2ba2027af8bb6a59d91eb3541ea4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/7922b53e70d2ba2027af8bb6a59d91eb3541ea4d", + "reference": "7922b53e70d2ba2027af8bb6a59d91eb3541ea4d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T09:27:11+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/5cefb712a25f320579615ba9e1942abaeade7dff", + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T07:20:23+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "b198dd66c211c97119bcaaff7c13431dbbb5e470" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/b198dd66c211c97119bcaaff7c13431dbbb5e470", + "reference": "b198dd66c211c97119bcaaff7c13431dbbb5e470", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T07:20:23+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-26T13:13:48+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T17:25:58+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T17:25:58+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T18:47:49+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-26T13:10:57+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grรฉgoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "d9593c9efa40499eb078b81144de42cbc28a31f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/d9593c9efa40499eb078b81144de42cbc28a31f0", + "reference": "d9593c9efa40499eb078b81144de42cbc28a31f0", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-11T16:55:21+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204", + "reference": "3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T07:20:23+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-28T09:44:51+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "965f7306a43383d02c6aca1e3f3bd2f0ea5dee15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/965f7306a43383d02c6aca1e3f3bd2f0ea5dee15", + "reference": "965f7306a43383d02c6aca1e3f3bd2f0ea5dee15", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-13T12:04:42+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/ada7578c30dd5feaa8259cff3e885069ea81ddde", + "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.4.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-06T11:19:24+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T13:30:16+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "2676b524340abcfe4d6151ec698463cebafee439" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/2676b524340abcfe4d6151ec698463cebafee439", + "reference": "2676b524340abcfe4d6151ec698463cebafee439", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grรฉgoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-30T15:19:22+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T13:44:50+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2026-04-26T05:33:54+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franรงois Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/breeze", + "version": "v2.4.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/breeze.git", + "reference": "4f20e7b2cc8d25daa85d8647241a89c8e0930305" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/breeze/zipball/4f20e7b2cc8d25daa85d8647241a89c8e0930305", + "reference": "4f20e7b2cc8d25daa85d8647241a89c8e0930305", + "shasum": "" + }, + "require": { + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/filesystem": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "illuminate/validation": "^11.0|^12.0|^13.0", + "php": "^8.2.0", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^11.0|^12.0|^13.0", + "orchestra/testbench-core": "^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Breeze\\BreezeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Breeze\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.", + "keywords": [ + "auth", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/breeze/issues", + "source": "https://github.com/laravel/breeze" + }, + "time": "2026-05-14T16:54:25+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-02-09T13:44:54+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.29.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/0770e9b7fafd50d4586881d456d6eb41c9247a80", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.95.1", + "illuminate/view": "^12.56.0", + "larastan/larastan": "^3.9.6", + "laravel-zero/framework": "^12.1.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.3" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-04-20T15:26:14+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.60.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "2a1538ed22eed4210ac1e17904235032571bd89c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/2a1538ed22eed4210ac1e17904235032571bd89c", + "reference": "2a1538ed22eed4210ac1e17904235032571bd89c", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/yaml": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" + }, + "bin": [ + "bin/sail" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sail\\SailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Docker files for running a basic Laravel application.", + "keywords": [ + "docker", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/sail/issues", + "source": "https://github.com/laravel/sail" + }, + "time": "2026-05-14T17:29:51+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pรกdraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.4", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.8 || ^8.0.8" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.6", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", + "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-04-21T14:04:20+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:37:06+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "8b6952b56ca6417f25f7a65758cadd0ce02edc51" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/8b6952b56ca6417f25f7a65758cadd0ce02edc51", + "reference": "8b6952b56ca6417f25f7a65758cadd0ce02edc51", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T07:20:23+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.2", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/laundry-app/config/app.php b/laundry-app/config/app.php new file mode 100644 index 0000000..c74a68d --- /dev/null +++ b/laundry-app/config/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'Asia/Jakarta', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/laundry-app/config/auth.php b/laundry-app/config/auth.php new file mode 100644 index 0000000..7d1eb0d --- /dev/null +++ b/laundry-app/config/auth.php @@ -0,0 +1,115 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', App\Models\User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/laundry-app/config/cache.php b/laundry-app/config/cache.php new file mode 100644 index 0000000..b32aead --- /dev/null +++ b/laundry-app/config/cache.php @@ -0,0 +1,117 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", + | "failover", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + +]; diff --git a/laundry-app/config/database.php b/laundry-app/config/database.php new file mode 100644 index 0000000..df933e7 --- /dev/null +++ b/laundry-app/config/database.php @@ -0,0 +1,183 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => env('DB_SSLMODE', 'prefer'), + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + ], + +]; diff --git a/laundry-app/config/filesystems.php b/laundry-app/config/filesystems.php new file mode 100644 index 0000000..37d8fca --- /dev/null +++ b/laundry-app/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/laundry-app/config/logging.php b/laundry-app/config/logging.php new file mode 100644 index 0000000..9e998a4 --- /dev/null +++ b/laundry-app/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/laundry-app/config/mail.php b/laundry-app/config/mail.php new file mode 100644 index 0000000..522b284 --- /dev/null +++ b/laundry-app/config/mail.php @@ -0,0 +1,118 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + +]; diff --git a/laundry-app/config/queue.php b/laundry-app/config/queue.php new file mode 100644 index 0000000..79c2c0a --- /dev/null +++ b/laundry-app/config/queue.php @@ -0,0 +1,129 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/laundry-app/config/sanctum.php b/laundry-app/config/sanctum.php new file mode 100644 index 0000000..44527d6 --- /dev/null +++ b/laundry-app/config/sanctum.php @@ -0,0 +1,84 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/laundry-app/config/services.php b/laundry-app/config/services.php new file mode 100644 index 0000000..1abcafc --- /dev/null +++ b/laundry-app/config/services.php @@ -0,0 +1,47 @@ + [ + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + 'google' => [ + 'client_id' => env('GOOGLE_CLIENT_ID'), + 'client_secret' => env('GOOGLE_CLIENT_SECRET'), + 'redirect' => env('GOOGLE_REDIRECT_URL'), + ], + + 'brevo' => [ + 'api_key' => env('BREVO_API_KEY'), + ], + +]; diff --git a/laundry-app/config/session.php b/laundry-app/config/session.php new file mode 100644 index 0000000..5b541b7 --- /dev/null +++ b/laundry-app/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug((string) env('APP_NAME', 'laravel')).'-session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain without subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/laundry-app/database/.gitignore b/laundry-app/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/laundry-app/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/laundry-app/database/factories/UserFactory.php b/laundry-app/database/factories/UserFactory.php new file mode 100644 index 0000000..584104c --- /dev/null +++ b/laundry-app/database/factories/UserFactory.php @@ -0,0 +1,44 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/laundry-app/database/migrations/0001_01_01_000000_create_users_table.php b/laundry-app/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..a81b9c8 --- /dev/null +++ b/laundry-app/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,61 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->string('password'); + $table->timestamp('email_verified_at')->nullable(); + + // Role: owner, staf, driver + $table->enum('role', ['owner', 'staf', 'driver'])->default('staf'); + + // Relasi hierarki: Staf/Driver terhubung ke Owner + // Jika user adalah 'owner', maka kolom ini NULL. + // Jika user adalah 'staf' atau 'driver', kolom ini berisi ID Owner mereka. + $table->unsignedBigInteger('owner_id')->nullable(); + + $table->rememberToken(); + $table->timestamps(); + + // Relasi ke tabel itu sendiri + $table->foreign('owner_id')->references('id')->on('users')->onDelete('cascade'); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/laundry-app/database/migrations/0001_01_01_000001_create_cache_table.php b/laundry-app/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..ed758bd --- /dev/null +++ b/laundry-app/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration')->index(); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/laundry-app/database/migrations/0001_01_01_000002_create_jobs_table.php b/laundry-app/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..425e705 --- /dev/null +++ b/laundry-app/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/laundry-app/database/migrations/2026_02_08_073500_create_shops_table.php b/laundry-app/database/migrations/2026_02_08_073500_create_shops_table.php new file mode 100644 index 0000000..7719653 --- /dev/null +++ b/laundry-app/database/migrations/2026_02_08_073500_create_shops_table.php @@ -0,0 +1,43 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); // Owner ID + $table->string('shop_name')->nullable(); + $table->decimal('latitude', 10, 8)->nullable(); + $table->decimal('longitude', 11, 8)->nullable(); + $table->unsignedInteger('delivery_fee_per_km')->default(0); + $table->integer('free_delivery_km')->default(0); + $table->boolean('is_delivery_active')->default(false); + $table->boolean('is_estimation_active')->default(false); + $table->integer('express_extra_price')->default(0); + $table->integer('kilat_extra_price')->default(0); + $table->string('device_id')->nullable(); + $table->timestamps(); + + // Satu owner hanya punya satu toko + $table->unique('user_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('shops'); + } +}; diff --git a/laundry-app/database/migrations/2026_02_08_073610_create_services_table.php b/laundry-app/database/migrations/2026_02_08_073610_create_services_table.php new file mode 100644 index 0000000..c175418 --- /dev/null +++ b/laundry-app/database/migrations/2026_02_08_073610_create_services_table.php @@ -0,0 +1,28 @@ +id(); + // Menghubungkan jasa ke Owner (User) + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + + $table->string('name'); + $table->unsignedInteger('price'); + $table->string('unit'); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('services'); + } +}; \ No newline at end of file diff --git a/laundry-app/database/migrations/2026_02_08_073659_create_orders_table.php b/laundry-app/database/migrations/2026_02_08_073659_create_orders_table.php new file mode 100644 index 0000000..b78cc08 --- /dev/null +++ b/laundry-app/database/migrations/2026_02_08_073659_create_orders_table.php @@ -0,0 +1,62 @@ +id(); + + // FK ke tabel users (Owner pemilik order ini) + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + + $table->string('order_number')->unique(); + $table->string('customer_name'); + $table->string('phone')->nullable(); + + // Layanan sekarang dipindah ke tabel order_items (1-to-many) + // Hanya menyimpan total harga dan total berat + $table->timestamp('estimation_time')->nullable(); + $table->decimal('total_weight', 8, 3)->default(0); + + // Harga & Pembayaran (unsigned karena tidak pernah negatif) + $table->unsignedInteger('delivery_fee')->default(0); + $table->unsignedInteger('total_price')->default(0); + $table->string('status')->default('ANTRIAN')->index(); + $table->string('payment_method')->default('CASH'); + $table->unsignedInteger('cash_received')->default(0); + $table->unsignedInteger('cash_change')->default(0); + + // Tipe Pengantaran (satu kolom saja, label Indonesia ditampilkan di UI) + // Values: none, pickup, delivery, both + $table->string('delivery_type')->default('none')->index(); + + // Kolom Lokasi untuk Fitur Map & Tracking Driver + $table->text('address')->nullable(); + $table->decimal('latitude', 10, 8)->nullable(); + $table->decimal('longitude', 11, 8)->nullable(); + $table->decimal('delivery_distance', 8, 2)->nullable(); // Jarak toko-pelanggan (km) + + $table->timestamps(); + + // Index untuk performa query laporan + $table->index('payment_method'); + $table->index('created_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('orders'); + } +}; \ No newline at end of file diff --git a/laundry-app/database/migrations/2026_02_14_044632_create_personal_access_tokens_table.php b/laundry-app/database/migrations/2026_02_14_044632_create_personal_access_tokens_table.php new file mode 100644 index 0000000..40ff706 --- /dev/null +++ b/laundry-app/database/migrations/2026_02_14_044632_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/laundry-app/database/migrations/2026_05_24_113000_create_order_items_table.php b/laundry-app/database/migrations/2026_05_24_113000_create_order_items_table.php new file mode 100644 index 0000000..bf3c45a --- /dev/null +++ b/laundry-app/database/migrations/2026_05_24_113000_create_order_items_table.php @@ -0,0 +1,36 @@ +id(); + + // Relasi ke order + $table->foreignId('order_id')->constrained()->onDelete('cascade'); + + // Relasi opsional ke services + $table->foreignId('service_id')->nullable()->constrained()->nullOnDelete(); + + // Snapshot data untuk histori jika suatu saat service diubah/dihapus + $table->string('service_name'); + $table->unsignedInteger('price'); + $table->decimal('qty_or_weight', 8, 3); + $table->string('unit'); // 'PCS' atau 'KG' + $table->unsignedInteger('subtotal'); + $table->string('estimation_name')->nullable(); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('order_items'); + } +}; diff --git a/laundry-app/database/migrations/2026_06_05_134732_drop_unused_columns_from_shops_table.php b/laundry-app/database/migrations/2026_06_05_134732_drop_unused_columns_from_shops_table.php new file mode 100644 index 0000000..7fea749 --- /dev/null +++ b/laundry-app/database/migrations/2026_06_05_134732_drop_unused_columns_from_shops_table.php @@ -0,0 +1,30 @@ +dropColumn([ + 'device_id', + ]); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('shops', function (Blueprint $table) { + $table->string('device_id')->nullable(); + }); + } +}; diff --git a/laundry-app/database/seeders/DatabaseSeeder.php b/laundry-app/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..fd8c967 --- /dev/null +++ b/laundry-app/database/seeders/DatabaseSeeder.php @@ -0,0 +1,21 @@ + 'These credentials do not match our records.', + 'password' => 'The provided password is incorrect.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + +]; diff --git a/laundry-app/lang/en/pagination.php b/laundry-app/lang/en/pagination.php new file mode 100644 index 0000000..d481411 --- /dev/null +++ b/laundry-app/lang/en/pagination.php @@ -0,0 +1,19 @@ + '« Previous', + 'next' => 'Next »', + +]; diff --git a/laundry-app/lang/en/passwords.php b/laundry-app/lang/en/passwords.php new file mode 100644 index 0000000..c21e98b --- /dev/null +++ b/laundry-app/lang/en/passwords.php @@ -0,0 +1,22 @@ + 'Kata sandi Anda telah berhasil diatur ulang.', + 'sent' => 'Kami telah mengirimkan tautan pengaturan ulang kata sandi ke email Anda.', + 'throttled' => 'Silakan tunggu sebelum mencoba lagi.', + 'token' => 'Token pengaturan ulang kata sandi ini tidak valid.', + 'user' => 'Kami tidak dapat menemukan pengguna dengan alamat email tersebut.', + +]; diff --git a/laundry-app/lang/en/validation.php b/laundry-app/lang/en/validation.php new file mode 100644 index 0000000..63ec29a --- /dev/null +++ b/laundry-app/lang/en/validation.php @@ -0,0 +1,200 @@ + 'The :attribute field must be accepted.', + 'accepted_if' => 'The :attribute field must be accepted when :other is :value.', + 'active_url' => 'The :attribute field must be a valid URL.', + 'after' => 'The :attribute field must be a date after :date.', + 'after_or_equal' => 'The :attribute field must be a date after or equal to :date.', + 'alpha' => 'The :attribute field must only contain letters.', + 'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.', + 'alpha_num' => 'The :attribute field must only contain letters and numbers.', + 'any_of' => 'The :attribute field is invalid.', + 'array' => 'The :attribute field must be an array.', + 'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.', + 'before' => 'The :attribute field must be a date before :date.', + 'before_or_equal' => 'The :attribute field must be a date before or equal to :date.', + 'between' => [ + 'array' => 'The :attribute field must have between :min and :max items.', + 'file' => 'The :attribute field must be between :min and :max kilobytes.', + 'numeric' => 'The :attribute field must be between :min and :max.', + 'string' => 'The :attribute field must be between :min and :max characters.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'can' => 'The :attribute field contains an unauthorized value.', + 'confirmed' => 'The :attribute field confirmation does not match.', + 'contains' => 'The :attribute field is missing a required value.', + 'current_password' => 'The password is incorrect.', + 'date' => 'The :attribute field must be a valid date.', + 'date_equals' => 'The :attribute field must be a date equal to :date.', + 'date_format' => 'The :attribute field must match the format :format.', + 'decimal' => 'The :attribute field must have :decimal decimal places.', + 'declined' => 'The :attribute field must be declined.', + 'declined_if' => 'The :attribute field must be declined when :other is :value.', + 'different' => 'The :attribute field and :other must be different.', + 'digits' => 'The :attribute field must be :digits digits.', + 'digits_between' => 'The :attribute field must be between :min and :max digits.', + 'dimensions' => 'The :attribute field has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'doesnt_contain' => 'The :attribute field must not contain any of the following: :values.', + 'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.', + 'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.', + 'email' => 'The :attribute field must be a valid email address.', + 'encoding' => 'The :attribute field must be encoded in :encoding.', + 'ends_with' => 'The :attribute field must end with one of the following: :values.', + 'enum' => 'The selected :attribute is invalid.', + 'exists' => 'The selected :attribute is invalid.', + 'extensions' => 'The :attribute field must have one of the following extensions: :values.', + 'file' => 'The :attribute field must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'array' => 'The :attribute field must have more than :value items.', + 'file' => 'The :attribute field must be greater than :value kilobytes.', + 'numeric' => 'The :attribute field must be greater than :value.', + 'string' => 'The :attribute field must be greater than :value characters.', + ], + 'gte' => [ + 'array' => 'The :attribute field must have :value items or more.', + 'file' => 'The :attribute field must be greater than or equal to :value kilobytes.', + 'numeric' => 'The :attribute field must be greater than or equal to :value.', + 'string' => 'The :attribute field must be greater than or equal to :value characters.', + ], + 'hex_color' => 'The :attribute field must be a valid hexadecimal color.', + 'image' => 'The :attribute field must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field must exist in :other.', + 'in_array_keys' => 'The :attribute field must contain at least one of the following keys: :values.', + 'integer' => 'The :attribute field must be an integer.', + 'ip' => 'The :attribute field must be a valid IP address.', + 'ipv4' => 'The :attribute field must be a valid IPv4 address.', + 'ipv6' => 'The :attribute field must be a valid IPv6 address.', + 'json' => 'The :attribute field must be a valid JSON string.', + 'list' => 'The :attribute field must be a list.', + 'lowercase' => 'The :attribute field must be lowercase.', + 'lt' => [ + 'array' => 'The :attribute field must have less than :value items.', + 'file' => 'The :attribute field must be less than :value kilobytes.', + 'numeric' => 'The :attribute field must be less than :value.', + 'string' => 'The :attribute field must be less than :value characters.', + ], + 'lte' => [ + 'array' => 'The :attribute field must not have more than :value items.', + 'file' => 'The :attribute field must be less than or equal to :value kilobytes.', + 'numeric' => 'The :attribute field must be less than or equal to :value.', + 'string' => 'The :attribute field must be less than or equal to :value characters.', + ], + 'mac_address' => 'The :attribute field must be a valid MAC address.', + 'max' => [ + 'array' => 'The :attribute field must not have more than :max items.', + 'file' => 'The :attribute field must not be greater than :max kilobytes.', + 'numeric' => 'The :attribute field must not be greater than :max.', + 'string' => 'The :attribute field must not be greater than :max characters.', + ], + 'max_digits' => 'The :attribute field must not have more than :max digits.', + 'mimes' => 'The :attribute field must be a file of type: :values.', + 'mimetypes' => 'The :attribute field must be a file of type: :values.', + 'min' => [ + 'array' => 'The :attribute field must have at least :min items.', + 'file' => 'The :attribute field must be at least :min kilobytes.', + 'numeric' => 'The :attribute field must be at least :min.', + 'string' => 'The :attribute field must be at least :min characters.', + ], + 'min_digits' => 'The :attribute field must have at least :min digits.', + 'missing' => 'The :attribute field must be missing.', + 'missing_if' => 'The :attribute field must be missing when :other is :value.', + 'missing_unless' => 'The :attribute field must be missing unless :other is :value.', + 'missing_with' => 'The :attribute field must be missing when :values is present.', + 'missing_with_all' => 'The :attribute field must be missing when :values are present.', + 'multiple_of' => 'The :attribute field must be a multiple of :value.', + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute field format is invalid.', + 'numeric' => 'The :attribute field must be a number.', + 'password' => [ + 'letters' => 'The :attribute field must contain at least one letter.', + 'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.', + 'numbers' => 'The :attribute field must contain at least one number.', + 'symbols' => 'The :attribute field must contain at least one symbol.', + 'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.', + ], + 'present' => 'The :attribute field must be present.', + 'present_if' => 'The :attribute field must be present when :other is :value.', + 'present_unless' => 'The :attribute field must be present unless :other is :value.', + 'present_with' => 'The :attribute field must be present when :values is present.', + 'present_with_all' => 'The :attribute field must be present when :values are present.', + 'prohibited' => 'The :attribute field is prohibited.', + 'prohibited_if' => 'The :attribute field is prohibited when :other is :value.', + 'prohibited_if_accepted' => 'The :attribute field is prohibited when :other is accepted.', + 'prohibited_if_declined' => 'The :attribute field is prohibited when :other is declined.', + 'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.', + 'prohibits' => 'The :attribute field prohibits :other from being present.', + 'regex' => 'The :attribute field format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_array_keys' => 'The :attribute field must contain entries for: :values.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_if_accepted' => 'The :attribute field is required when :other is accepted.', + 'required_if_declined' => 'The :attribute field is required when :other is declined.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute field must match :other.', + 'size' => [ + 'array' => 'The :attribute field must contain :size items.', + 'file' => 'The :attribute field must be :size kilobytes.', + 'numeric' => 'The :attribute field must be :size.', + 'string' => 'The :attribute field must be :size characters.', + ], + 'starts_with' => 'The :attribute field must start with one of the following: :values.', + 'string' => 'The :attribute field must be a string.', + 'timezone' => 'The :attribute field must be a valid timezone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'uppercase' => 'The :attribute field must be uppercase.', + 'url' => 'The :attribute field must be a valid URL.', + 'ulid' => 'The :attribute field must be a valid ULID.', + 'uuid' => 'The :attribute field must be a valid UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + +]; diff --git a/laundry-app/package-lock.json b/laundry-app/package-lock.json new file mode 100644 index 0000000..e72f2d2 --- /dev/null +++ b/laundry-app/package-lock.json @@ -0,0 +1,3246 @@ +{ + "name": "laundry-app", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@tailwindcss/forms": "^0.5.2", + "@tailwindcss/vite": "^4.0.0", + "alpinejs": "^3.4.2", + "autoprefixer": "^10.4.2", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "postcss": "^8.4.31", + "tailwindcss": "^3.1.0", + "vite": "^7.0.7" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz", + "integrity": "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==", + "dev": true, + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/node/node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "dev": true, + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tailwindcss/vite/node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@vue/reactivity": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", + "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", + "dev": true, + "dependencies": { + "@vue/shared": "3.1.5" + } + }, + "node_modules/@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", + "dev": true + }, + "node_modules/alpinejs": { + "version": "3.15.8", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.8.tgz", + "integrity": "sha512-zxIfCRTBGvF1CCLIOMQOxAyBuqibxSEwS6Jm1a3HGA9rgrJVcjEWlwLcQTVGAWGS8YhAsTRLVrtQ5a5QT9bSSQ==", + "dev": true, + "dependencies": { + "@vue/reactivity": "~3.1.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/laravel-vite-plugin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.1.0.tgz", + "integrity": "sha512-z+ck2BSV6KWtYcoIzk9Y5+p4NEjqM+Y4i8/H+VZRLq0OgNjW2DqyADquwYu5j8qRvaXwzNmfCWl1KrMlV1zpsg==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^7.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + } + } +} diff --git a/laundry-app/package.json b/laundry-app/package.json new file mode 100644 index 0000000..2ea7e1d --- /dev/null +++ b/laundry-app/package.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://www.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.2", + "@tailwindcss/vite": "^4.0.0", + "alpinejs": "^3.4.2", + "autoprefixer": "^10.4.2", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "postcss": "^8.4.31", + "tailwindcss": "^3.1.0", + "vite": "^7.0.7" + } +} diff --git a/laundry-app/phpunit.xml b/laundry-app/phpunit.xml new file mode 100644 index 0000000..d703241 --- /dev/null +++ b/laundry-app/phpunit.xml @@ -0,0 +1,35 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + diff --git a/laundry-app/postcss.config.js b/laundry-app/postcss.config.js new file mode 100644 index 0000000..49c0612 --- /dev/null +++ b/laundry-app/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/laundry-app/public/.htaccess b/laundry-app/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/laundry-app/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/laundry-app/public/app_icon.png b/laundry-app/public/app_icon.png new file mode 100644 index 0000000..8109f2c Binary files /dev/null and b/laundry-app/public/app_icon.png differ diff --git a/laundry-app/public/favicon.ico b/laundry-app/public/favicon.ico new file mode 100644 index 0000000..8109f2c Binary files /dev/null and b/laundry-app/public/favicon.ico differ diff --git a/laundry-app/public/index.php b/laundry-app/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/laundry-app/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/laundry-app/public/robots.txt b/laundry-app/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/laundry-app/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/laundry-app/resources/css/app.css b/laundry-app/resources/css/app.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/laundry-app/resources/css/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/laundry-app/resources/js/app.js b/laundry-app/resources/js/app.js new file mode 100644 index 0000000..a8093be --- /dev/null +++ b/laundry-app/resources/js/app.js @@ -0,0 +1,7 @@ +import './bootstrap'; + +import Alpine from 'alpinejs'; + +window.Alpine = Alpine; + +Alpine.start(); diff --git a/laundry-app/resources/js/bootstrap.js b/laundry-app/resources/js/bootstrap.js new file mode 100644 index 0000000..5f1390b --- /dev/null +++ b/laundry-app/resources/js/bootstrap.js @@ -0,0 +1,4 @@ +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/laundry-app/resources/views/auth/confirm-password.blade.php b/laundry-app/resources/views/auth/confirm-password.blade.php new file mode 100644 index 0000000..3d38186 --- /dev/null +++ b/laundry-app/resources/views/auth/confirm-password.blade.php @@ -0,0 +1,27 @@ + +
+ {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} +
+ +
+ @csrf + + +
+ + + + + +
+ +
+ + {{ __('Confirm') }} + +
+
+
diff --git a/laundry-app/resources/views/auth/forgot-password.blade.php b/laundry-app/resources/views/auth/forgot-password.blade.php new file mode 100644 index 0000000..5c031c6 --- /dev/null +++ b/laundry-app/resources/views/auth/forgot-password.blade.php @@ -0,0 +1,77 @@ + + + + + + Lupa Kata Sandi - Laundry System + + + + + + + +
+
+
+
+ +
+
+
+ ๐Ÿ”‘ +
+

Lupa Kata Sandi?

+

+ Masukkan email Anda dan kami akan mengirimkan tautan untuk mengatur ulang kata sandi Anda. +

+
+ + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + +
+ @csrf + +
+ + + @error('email') +

{{ $message }}

+ @enderror +
+ + + + +
+
+
+ + + \ No newline at end of file diff --git a/laundry-app/resources/views/auth/login.blade.php b/laundry-app/resources/views/auth/login.blade.php new file mode 100644 index 0000000..e068e2c --- /dev/null +++ b/laundry-app/resources/views/auth/login.blade.php @@ -0,0 +1,136 @@ + + + + + + + Login - Laundry System + + + + + + + + + + + +
+
+
+
+
+ +
+
+

Login

+

Kelola pesanan, layanan, dan laporan Anda dengan efisien.

+
+ + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + +
+ @csrf + +
+ + + @error('email') +

{{ $message }}

+ @enderror +
+ +
+ +
+ + +
+ visibility +
+
+ @error('password') +

{{ $message }}

+ @enderror +
+ +
+ + + @if (Route::has('password.request')) + Lupa Kata Sandi? + @endif +
+ + + +
+

+ Belum punya akun? + + Daftar Sekarang + +

+
+
+
+
+ + + + \ No newline at end of file diff --git a/laundry-app/resources/views/auth/register.blade.php b/laundry-app/resources/views/auth/register.blade.php new file mode 100644 index 0000000..6d452ec --- /dev/null +++ b/laundry-app/resources/views/auth/register.blade.php @@ -0,0 +1,119 @@ + + + + + + Daftar Akun - Laundry System + + + + + + + + + +
+
+
+
+
+ +
+
+

Daftar Akun

+

Buat akun untuk mulai mengelola sistem laundry Anda.

+
+ +
+ @csrf + +
+ + + @error('name')

{{ $message }}

@enderror +
+ +
+ + + @error('email')

{{ $message }}

@enderror +
+ +
+ +
+ +
+ visibility +
+
+ @error('password')

{{ $message }}

@enderror +
+ +
+ +
+ +
+ visibility +
+
+
+ + + +
+

+ Sudah memiliki akun? + + Masuk di sini + +

+
+
+
+
+ + + + + \ No newline at end of file diff --git a/laundry-app/resources/views/auth/reset-password.blade.php b/laundry-app/resources/views/auth/reset-password.blade.php new file mode 100644 index 0000000..11af45f --- /dev/null +++ b/laundry-app/resources/views/auth/reset-password.blade.php @@ -0,0 +1,39 @@ + +
+ @csrf + + + + + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + + +
+ +
+ + {{ __('Reset Password') }} + +
+
+
diff --git a/laundry-app/resources/views/auth/verify-email.blade.php b/laundry-app/resources/views/auth/verify-email.blade.php new file mode 100644 index 0000000..eaf811d --- /dev/null +++ b/laundry-app/resources/views/auth/verify-email.blade.php @@ -0,0 +1,31 @@ + +
+ {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} +
+ + @if (session('status') == 'verification-link-sent') +
+ {{ __('A new verification link has been sent to the email address you provided during registration.') }} +
+ @endif + +
+
+ @csrf + +
+ + {{ __('Resend Verification Email') }} + +
+
+ +
+ @csrf + + +
+
+
diff --git a/laundry-app/resources/views/components/application-logo.blade.php b/laundry-app/resources/views/components/application-logo.blade.php new file mode 100644 index 0000000..d7ad58c --- /dev/null +++ b/laundry-app/resources/views/components/application-logo.blade.php @@ -0,0 +1 @@ +Logo diff --git a/laundry-app/resources/views/components/auth-session-status.blade.php b/laundry-app/resources/views/components/auth-session-status.blade.php new file mode 100644 index 0000000..c4bd6e2 --- /dev/null +++ b/laundry-app/resources/views/components/auth-session-status.blade.php @@ -0,0 +1,7 @@ +@props(['status']) + +@if ($status) +
merge(['class' => 'font-medium text-sm text-green-600']) }}> + {{ $status }} +
+@endif diff --git a/laundry-app/resources/views/components/danger-button.blade.php b/laundry-app/resources/views/components/danger-button.blade.php new file mode 100644 index 0000000..d17d288 --- /dev/null +++ b/laundry-app/resources/views/components/danger-button.blade.php @@ -0,0 +1,3 @@ + diff --git a/laundry-app/resources/views/components/dropdown-link.blade.php b/laundry-app/resources/views/components/dropdown-link.blade.php new file mode 100644 index 0000000..e0f8ce1 --- /dev/null +++ b/laundry-app/resources/views/components/dropdown-link.blade.php @@ -0,0 +1 @@ +merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }} diff --git a/laundry-app/resources/views/components/dropdown.blade.php b/laundry-app/resources/views/components/dropdown.blade.php new file mode 100644 index 0000000..a46f7c8 --- /dev/null +++ b/laundry-app/resources/views/components/dropdown.blade.php @@ -0,0 +1,35 @@ +@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white']) + +@php +$alignmentClasses = match ($align) { + 'left' => 'ltr:origin-top-left rtl:origin-top-right start-0', + 'top' => 'origin-top', + default => 'ltr:origin-top-right rtl:origin-top-left end-0', +}; + +$width = match ($width) { + '48' => 'w-48', + default => $width, +}; +@endphp + +
+
+ {{ $trigger }} +
+ + +
diff --git a/laundry-app/resources/views/components/input-error.blade.php b/laundry-app/resources/views/components/input-error.blade.php new file mode 100644 index 0000000..9e6da21 --- /dev/null +++ b/laundry-app/resources/views/components/input-error.blade.php @@ -0,0 +1,9 @@ +@props(['messages']) + +@if ($messages) + +@endif diff --git a/laundry-app/resources/views/components/input-label.blade.php b/laundry-app/resources/views/components/input-label.blade.php new file mode 100644 index 0000000..1cc65e2 --- /dev/null +++ b/laundry-app/resources/views/components/input-label.blade.php @@ -0,0 +1,5 @@ +@props(['value']) + + diff --git a/laundry-app/resources/views/components/modal.blade.php b/laundry-app/resources/views/components/modal.blade.php new file mode 100644 index 0000000..70704c1 --- /dev/null +++ b/laundry-app/resources/views/components/modal.blade.php @@ -0,0 +1,78 @@ +@props([ + 'name', + 'show' => false, + 'maxWidth' => '2xl' +]) + +@php +$maxWidth = [ + 'sm' => 'sm:max-w-sm', + 'md' => 'sm:max-w-md', + 'lg' => 'sm:max-w-lg', + 'xl' => 'sm:max-w-xl', + '2xl' => 'sm:max-w-2xl', +][$maxWidth]; +@endphp + +
+
+
+
+ +
+ {{ $slot }} +
+
diff --git a/laundry-app/resources/views/components/nav-link.blade.php b/laundry-app/resources/views/components/nav-link.blade.php new file mode 100644 index 0000000..5c101a2 --- /dev/null +++ b/laundry-app/resources/views/components/nav-link.blade.php @@ -0,0 +1,11 @@ +@props(['active']) + +@php +$classes = ($active ?? false) + ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out' + : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out'; +@endphp + +merge(['class' => $classes]) }}> + {{ $slot }} + diff --git a/laundry-app/resources/views/components/primary-button.blade.php b/laundry-app/resources/views/components/primary-button.blade.php new file mode 100644 index 0000000..d71f0b6 --- /dev/null +++ b/laundry-app/resources/views/components/primary-button.blade.php @@ -0,0 +1,3 @@ + diff --git a/laundry-app/resources/views/components/responsive-nav-link.blade.php b/laundry-app/resources/views/components/responsive-nav-link.blade.php new file mode 100644 index 0000000..43b91e7 --- /dev/null +++ b/laundry-app/resources/views/components/responsive-nav-link.blade.php @@ -0,0 +1,11 @@ +@props(['active']) + +@php +$classes = ($active ?? false) + ? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out' + : 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out'; +@endphp + +merge(['class' => $classes]) }}> + {{ $slot }} + diff --git a/laundry-app/resources/views/components/secondary-button.blade.php b/laundry-app/resources/views/components/secondary-button.blade.php new file mode 100644 index 0000000..b32b69f --- /dev/null +++ b/laundry-app/resources/views/components/secondary-button.blade.php @@ -0,0 +1,3 @@ + diff --git a/laundry-app/resources/views/components/text-input.blade.php b/laundry-app/resources/views/components/text-input.blade.php new file mode 100644 index 0000000..da1b12d --- /dev/null +++ b/laundry-app/resources/views/components/text-input.blade.php @@ -0,0 +1,3 @@ +@props(['disabled' => false]) + +merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) }}> diff --git a/laundry-app/resources/views/dashboard.blade.php b/laundry-app/resources/views/dashboard.blade.php new file mode 100644 index 0000000..0b164c7 --- /dev/null +++ b/laundry-app/resources/views/dashboard.blade.php @@ -0,0 +1,132 @@ + + {{-- Wrapper utama disamakan persis dengan halaman Kelola Satuan --}} +
+ + {{-- Header Section --}} +
+
+

Beranda

+

Ringkasan performa dan pesanan aktif laundry Anda.

+
+
+ + {{-- Statistik Ringkas --}} +
+ +
+ + {{-- Section Grafik Pelanggan --}} +
+
+

Grafik Pelanggan

+

Jumlah pelanggan harian (7 Hari Terakhir)

+
+ +
+ + + + + + + + + + + + + + + + + + + + + @foreach(explode(' ', $chartPoints) as $point) + @php $coord = explode(',', $point); @endphp + @if(count($coord) == 2) + + + @endif + @endforeach + +
+ +
+ @foreach($days as $index => $day) +
+ {{ $day }} + {{ $chartData[$index] }} +
+ @endforeach +
+
+ + +
+ + {{-- STYLE DIPINDAH KE BAWAH AGAR TIDAK MENDORONG JUDUL KE BAWAH --}} + +
\ No newline at end of file diff --git a/laundry-app/resources/views/laundry.blade.php b/laundry-app/resources/views/laundry.blade.php new file mode 100644 index 0000000..a831b6e --- /dev/null +++ b/laundry-app/resources/views/laundry.blade.php @@ -0,0 +1,239 @@ + +
+ @if(session('success')) +
+
+
+
+ check_circle +
+
+

Berhasil

+

{{ session('success') }}

+
+
+ +
+
+ @endif + +
+
+ {{-- Judul: normal-case (Bukan kapital semua) --}} +

Kelola Satuan Laundry

+

Atur daftar harga dan kategori layanan laundry Anda.

+
+
+ {{-- Tombol: Ukuran lebih kecil (px-4 py-2, text-xs) --}} + + add + Tambah Layanan Baru + +
+
+ +
+
+
+ search + +
+ +
+ + + + {{-- Mobile Card Layout --}} +
+ @forelse($services as $item) +
+
+
+

{{ $item->name }}

+ + {{ $item->unit }} + +
+
+ Rp {{ number_format($item->price, 0, ',', '.') }} + /{{ strtolower($item->unit) }} +
+
+ + edit Edit + +
+ @csrf @method('DELETE') + +
+
+
+
+
+ @csrf + +
+
+
+ @empty +
Belum ada layanan.
+ @endforelse +
+
+

+ Total: {{ $services->count() }} Layanan +

+
+
+
+ + + + + +
\ No newline at end of file diff --git a/laundry-app/resources/views/laundry/create.blade.php b/laundry-app/resources/views/laundry/create.blade.php new file mode 100644 index 0000000..c3ae999 --- /dev/null +++ b/laundry-app/resources/views/laundry/create.blade.php @@ -0,0 +1,76 @@ + +
+ {{-- Header --}} +
+ + + arrow_back + + +
+

Tambah Layanan Baru

+

Input data jasa laundry baru

+
+
+ +
+
+
+
+ @csrf + + {{-- Nama Layanan --}} +
+ + + @error('name')

{{ $message }}

@enderror +
+ + {{-- Grid untuk Satuan dan Harga --}} +
+ {{-- Satuan (Tanpa Meter) --}} +
+ +
+ {{-- PERBAIKAN: Tambahkan bg-none dan pr-10 di sini --}} + + {{-- Hanya 1 Panah ke Bawah (Ikon Custom) --}} +
+ keyboard_arrow_down +
+
+
+ + {{-- Harga --}} +
+ +
+
Rp
+ +
+
+
+ + {{-- Action Buttons --}} +
+ + Batal + + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/laundry-app/resources/views/laundry/edit.blade.php b/laundry-app/resources/views/laundry/edit.blade.php new file mode 100644 index 0000000..c6edd07 --- /dev/null +++ b/laundry-app/resources/views/laundry/edit.blade.php @@ -0,0 +1,88 @@ + +
+ {{-- Header: Disamakan dengan halaman Create --}} +
+ + + arrow_back + + +
+

Edit Layanan

+

Perbarui informasi jasa laundry

+
+
+ +
+
+
+
+ @csrf + @method('PUT') + + {{-- Nama Layanan --}} +
+ + + @error('name')

{{ $message }}

@enderror +
+ + {{-- Grid untuk Satuan dan Harga --}} +
+ {{-- Satuan --}} +
+ +
+ {{-- PERBAIKAN: Ditambahkan bg-none di sini --}} + + {{-- Hanya 1 Panah ke Bawah --}} +
+ keyboard_arrow_down +
+
+
+ + {{-- Harga --}} +
+ +
+
Rp
+ +
+ @error('price')

{{ $message }}

@enderror +
+
+ + {{-- Action Buttons: Di Sebelah Kanan --}} +
+ + Batal + + +
+
+
+
+
+
+ + +
\ No newline at end of file diff --git a/laundry-app/resources/views/layouts/app.blade.php b/laundry-app/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..643c6d0 --- /dev/null +++ b/laundry-app/resources/views/layouts/app.blade.php @@ -0,0 +1,253 @@ + + + + + + + My Laundry - Management System + + + + + + + @livewireStyles + + + + + + + + @if(session('success')) +
+
+
+
+ check_circle +
+
+

Berhasil

+

{{ session('success') }}

+
+
+ +
+
+ @endif + + @if(session('error')) +
+
+
+
+ error +
+
+

Gagal

+

{{ session('error') }}

+
+
+ +
+
+ @endif + + {{-- OVERLAY PUTAR LAYAR --}} + + +
+ +
+ + + +
+
+ +
+ Logo + My LAUNDRY +
+ +
+
+ +
+ {{ $slot }} +
+
+
+ + + @livewireScripts + + + + \ No newline at end of file diff --git a/laundry-app/resources/views/layouts/guest.blade.php b/laundry-app/resources/views/layouts/guest.blade.php new file mode 100644 index 0000000..4e4e77c --- /dev/null +++ b/laundry-app/resources/views/layouts/guest.blade.php @@ -0,0 +1,34 @@ + + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+
+ + + +
+ +
+ {{ $slot }} +
+
+ + diff --git a/laundry-app/resources/views/layouts/navigation.blade.php b/laundry-app/resources/views/layouts/navigation.blade.php new file mode 100644 index 0000000..c2d3a65 --- /dev/null +++ b/laundry-app/resources/views/layouts/navigation.blade.php @@ -0,0 +1,100 @@ + diff --git a/laundry-app/resources/views/livewire/active-orders.blade.php b/laundry-app/resources/views/livewire/active-orders.blade.php new file mode 100644 index 0000000..3a0c68e --- /dev/null +++ b/laundry-app/resources/views/livewire/active-orders.blade.php @@ -0,0 +1,206 @@ +
+
+
+

Pesanan Aktif Terbaru

+
+
+ + + {{-- Mobile Card Layout --}} +
+ @forelse($latest as $order) +
+
+
+ #{{ $order->order_number }} +

{{ $order->customer_name }}

+
+
+ @if($order->status == 'PROSES') + + PROSES + + @elseif($order->status == 'ANTRIAN') + + ANTRIAN + + @elseif($order->status == 'SELESAI') + + SELESAI + + @elseif(in_array($order->status, ['PICKUP', 'MENUNGGU JEMPUT'])) + + PICKUP + + @elseif($order->status == 'TIBA DI TOKO' || $order->status == 'JEMPUTAN TIBA') + + TIBA DI TOKO + + @else + + {{ $order->status }} + + @endif +
+
+ +
+ @if(in_array($order->status, ['PICKUP', 'MENUNGGU JEMPUT', 'TIBA DI TOKO', 'JEMPUTAN TIBA'])) + - + @else + {!! nl2br(e(explode('||', $order->service_name ?? 'Layanan Umum')[0])) !!} + @endif +
+ @if(!in_array($order->status, ['PICKUP', 'MENUNGGU JEMPUT', 'TIBA DI TOKO', 'JEMPUTAN TIBA'])) + @if(strtoupper($order->payment_method) == 'BAYAR NANTI' && $order->cash_received == 0) + BELUM BAYAR + @else + LUNAS + @endif + + @php + $isSatuan = preg_match('/PCS|Satuan/i', $order->service_name); + $tipeEstimasi = "Reguler"; + if (!$isSatuan) { + if ($order->estimation_time && $order->created_at) { + $diffInHours = $order->created_at->diffInHours($order->estimation_time); + if ($diffInHours <= 6) $tipeEstimasi = "Kilat"; + elseif ($diffInHours <= 24) $tipeEstimasi = "Ekspres"; + } else { + if (preg_match('/Kilat/i', $order->service_name)) $tipeEstimasi = "Kilat"; + elseif (preg_match('/Ekspres|Express/i', $order->service_name)) $tipeEstimasi = "Ekspres"; + } + } + @endphp + @if(!$isSatuan) + + {{ $tipeEstimasi }} + + @endif + @endif + + @if($order->delivery_type && $order->delivery_type !== 'none') + @php + $typeLabel = ""; + if ($order->delivery_type === 'pickup') $typeLabel = "Pick-up"; + elseif ($order->delivery_type === 'delivery') $typeLabel = "Delivery"; + elseif ($order->delivery_type === 'both') $typeLabel = "Pick-up & Delivery"; + @endphp + + {{ $typeLabel }} + + @endif +
+
+
+ @empty +
Belum ada pesanan aktif saat ini.
+ @endforelse +
+ +
+ {{ $latest->links() }} +
+
diff --git a/laundry-app/resources/views/livewire/dashboard-stats.blade.php b/laundry-app/resources/views/livewire/dashboard-stats.blade.php new file mode 100644 index 0000000..d73f713 --- /dev/null +++ b/laundry-app/resources/views/livewire/dashboard-stats.blade.php @@ -0,0 +1,48 @@ +
+
+ + {{-- Card Total Pesanan --}} +
+
+ format_list_numbered +
+
+

{{ number_format($total) }}

+

Total Pesanan

+
+
+ + {{-- Card Antrian (Klik-able) --}} + +
+ hourglass_top +
+
+

{{ $antrian }}

+

Antrian

+
+
+ + {{-- Card Proses (Klik-able) --}} + +
+ waves +
+
+

{{ $proses }}

+

Proses

+
+
+ + {{-- Card Selesai (Klik-able) --}} + +
+ check_circle +
+
+

{{ number_format($selesai) }}

+

Selesai

+
+
+
+
diff --git a/laundry-app/resources/views/orders/status_list.blade.php b/laundry-app/resources/views/orders/status_list.blade.php new file mode 100644 index 0000000..bdf461a --- /dev/null +++ b/laundry-app/resources/views/orders/status_list.blade.php @@ -0,0 +1,346 @@ + +
+ @if(session('success')) +
+
+
+
+ check_circle +
+
+

Berhasil

+

{{ session('success') }}

+
+
+ +
+
+ @endif +
+ + arrow_back + +

Daftar {{ ucfirst(strtolower($statusUpper)) }}

+
+ +
+ + {{-- Toolbar: Tombol Hapus (Kiri) & Search (Kanan) --}} +
+ + {{-- Tombol Hapus Terpilih --}} + + + {{-- Form Pencarian --}} +
+ search + + {{-- autocomplete="off" untuk menghapus riwayat saran browser --}} + +
+
+ + {{-- Form pembungkus untuk fitur hapus massal --}} +
+ @csrf + @method('DELETE') + + + + {{-- Mobile Card Layout --}} +
+ @forelse($orders as $order) +
+
+ +
+
+ #{{ $order->order_number }} +

{{ $order->customer_name }}

+
+
Masuk: {{ $order->created_at->format('d M, H:i') }}
+ @if($order->estimation_time) +
Selesai: {{ \Carbon\Carbon::parse($order->estimation_time)->format('d M, H:i') }}
+ @endif +
+
+ +
+ {!! nl2br(e(explode('||', $order->service_name)[0])) !!} +
+ @php + $isSatuan = preg_match('/PCS|Satuan/i', $order->service_name); + $tipeEstimasi = "Reguler"; + if (!$isSatuan) { + if ($order->estimation_time && $order->created_at) { + $diffInHours = $order->created_at->diffInHours($order->estimation_time); + if ($diffInHours <= 6) $tipeEstimasi = "Kilat"; + elseif ($diffInHours <= 24) $tipeEstimasi = "Ekspres"; + } else { + if (preg_match('/Kilat/i', $order->service_name)) $tipeEstimasi = "Kilat"; + elseif (preg_match('/Ekspres|Express/i', $order->service_name)) $tipeEstimasi = "Ekspres"; + } + } + @endphp + @if(!$isSatuan) + + {{ $tipeEstimasi }} + + @endif + + @if($order->delivery_type && $order->delivery_type !== 'none') + @php + $typeLabel = ""; + if ($order->delivery_type === 'pickup') $typeLabel = "Pick-up"; + elseif ($order->delivery_type === 'delivery') $typeLabel = "Delivery"; + elseif ($order->delivery_type === 'both') $typeLabel = "Pick-up & Delivery"; + @endphp + + {{ $typeLabel }} + + @endif +
+
+ +
+
+ Rp {{ number_format($order->total_price, 0, ',', '.') }} + @if(strtoupper($order->payment_method) == 'BAYAR NANTI' && $order->cash_received == 0) + BELUM BAYAR + @else + LUNAS ({{ strtoupper($order->payment_method) == 'CASH' ? 'TUNAI' : $order->payment_method }}) + @endif +
+
+ @php + $color = [ + 'ANTRIAN' => 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400', + 'PROSES' => 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', + 'SELESAI' => 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', + 'PICKUP' => 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400', + 'TIBA DI TOKO' => 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400', + 'JEMPUTAN TIBA' => 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400', + ][$order->status] ?? 'bg-gray-100'; + @endphp + + {{ $order->status }} + +
+
+
+ @empty +
+ Tidak ada data pesanan @if(request('search')) untuk "{{ request('search') }}" @endif. +
+ @endforelse +
+ + @if($orders->hasPages()) +
+ {{ $orders->appends(request()->query())->links() }} +
+ @endif +
+
+
+ + + +
\ No newline at end of file diff --git a/laundry-app/resources/views/profile/edit.blade.php b/laundry-app/resources/views/profile/edit.blade.php new file mode 100644 index 0000000..e0e1d38 --- /dev/null +++ b/laundry-app/resources/views/profile/edit.blade.php @@ -0,0 +1,29 @@ + + +

+ {{ __('Profile') }} +

+
+ +
+
+
+
+ @include('profile.partials.update-profile-information-form') +
+
+ +
+
+ @include('profile.partials.update-password-form') +
+
+ +
+
+ @include('profile.partials.delete-user-form') +
+
+
+
+
diff --git a/laundry-app/resources/views/profile/partials/delete-user-form.blade.php b/laundry-app/resources/views/profile/partials/delete-user-form.blade.php new file mode 100644 index 0000000..edeeb4a --- /dev/null +++ b/laundry-app/resources/views/profile/partials/delete-user-form.blade.php @@ -0,0 +1,55 @@ +
+
+

+ {{ __('Delete Account') }} +

+ +

+ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }} +

+
+ + {{ __('Delete Account') }} + + +
+ @csrf + @method('delete') + +

+ {{ __('Are you sure you want to delete your account?') }} +

+ +

+ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }} +

+ +
+ + + + + +
+ +
+ + {{ __('Cancel') }} + + + + {{ __('Delete Account') }} + +
+
+
+
diff --git a/laundry-app/resources/views/profile/partials/update-password-form.blade.php b/laundry-app/resources/views/profile/partials/update-password-form.blade.php new file mode 100644 index 0000000..e66aeff --- /dev/null +++ b/laundry-app/resources/views/profile/partials/update-password-form.blade.php @@ -0,0 +1,48 @@ +
+
+

+ {{ __('Update Password') }} +

+ +

+ {{ __('Ensure your account is using a long, random password to stay secure.') }} +

+
+ +
+ @csrf + @method('put') + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ {{ __('Save') }} + + @if (session('status') === 'password-updated') +

{{ __('Saved.') }}

+ @endif +
+
+
diff --git a/laundry-app/resources/views/profile/partials/update-profile-information-form.blade.php b/laundry-app/resources/views/profile/partials/update-profile-information-form.blade.php new file mode 100644 index 0000000..0394949 --- /dev/null +++ b/laundry-app/resources/views/profile/partials/update-profile-information-form.blade.php @@ -0,0 +1,64 @@ +
+
+

+ {{ __('Profile Information') }} +

+ +

+ {{ __("Update your account's profile information and email address.") }} +

+
+ +
+ @csrf +
+ +
+ @csrf + @method('patch') + +
+ + + +
+ +
+ + + + + @if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail()) +
+

+ {{ __('Your email address is unverified.') }} + + +

+ + @if (session('status') === 'verification-link-sent') +

+ {{ __('A new verification link has been sent to your email address.') }} +

+ @endif +
+ @endif +
+ +
+ {{ __('Save') }} + + @if (session('status') === 'profile-updated') +

{{ __('Saved.') }}

+ @endif +
+
+
diff --git a/laundry-app/resources/views/reports.blade.php b/laundry-app/resources/views/reports.blade.php new file mode 100644 index 0000000..f23ceb7 --- /dev/null +++ b/laundry-app/resources/views/reports.blade.php @@ -0,0 +1,499 @@ + +
+ {{-- Header Section --}} +
+
+

Laporan Pendapatan

+

+ Periode: {{ \Carbon\Carbon::parse($startDate)->format('d M') }} - {{ \Carbon\Carbon::parse($endDate)->format('d M Y') }} +

+
+
+ + {{-- Statistik Ringkas --}} +
+ {{-- Pendapatan Laundry --}} +
+
+
+ local_laundry_service +
+
+

Pendapatan Laundry

+

+ Rp {{ number_format($totalLaundryRevenue, 0, ',', '.') }} +

+
+ + {{-- Pendapatan Driver --}} +
+
+
+ two_wheeler +
+
+

Pendapatan Driver

+

+ Rp {{ number_format($totalDeliveryFee, 0, ',', '.') }} +

+
+ + {{-- Total Keseluruhan --}} +
+
+
+ payments +
+
+

Total Keseluruhan

+

+ Rp {{ number_format($totalRevenue, 0, ',', '.') }} +

+
+ + {{-- Total Pesanan --}} +
+
+
+ shopping_cart +
+
+

Total Pesanan

+

{{ $totalOrders }}

+
+
+ + {{-- Filter Tanggal --}} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ Reset + +
+
+
+ + {{-- Tabel Laporan --}} +
+ @csrf + @method('DELETE') +
+
+ {{-- Header Normal (Saat Tidak Ada Pilihan) --}} +
+

Data Detail Laporan

+ + table_view + Ekspor Semua + +
+ + {{-- Header Bulk Action (Muncul Saat Checklist Tercentang) --}} + +
+ + + + {{-- Mobile Card Layout --}} +
+ @php $currentMonthMobile = null; @endphp + + @forelse($reports as $report) + @php + $monthNameMobile = $report->updated_at->translatedFormat('F Y'); + @endphp + + @if($monthNameMobile !== $currentMonthMobile) +
+
+ + + {{ $monthNameMobile }} + +
+
+ @php $currentMonthMobile = $monthNameMobile; @endphp + @endif + +
+
+ +
+
+

#{{ $report->order_number ?? $report->invoice_number }}

+

{{ $report->customer_name }}

+

{{ $report->updated_at->format('d/m/Y H:i') }}

+
+ +
+ @if($report->items && $report->items->count() > 0) + @foreach($report->items as $item) + @php + $serviceName = explode('||', $item->service_name)[0]; + $unitDb = strtolower($item->unit ?? ''); + $isPcs = ($unitDb == 'pcs' || stripos($serviceName, 'satuan') !== false || stripos($serviceName, 'pcs') !== false); + $weightValue = $isPcs ? (int)$item->qty_or_weight : $item->qty_or_weight; + $alreadyHasUnit = preg_match('/\d+(\.\d+)?\s*(pcs|kg)/i', $serviceName); + @endphp + + @if($alreadyHasUnit) + {!! nl2br(e($serviceName)) !!} + @else + {!! nl2br(e($serviceName)) !!} ({{ $weightValue }} {{ $isPcs ? 'Pcs' : 'Kg' }}) + @endif + + @endforeach + @else + @php + $serviceName = explode('||', $report->service_name)[0]; + $unitDb = strtolower($report->unit ?? ''); + $isPcs = ($unitDb == 'pcs' || stripos($serviceName, 'satuan') !== false || stripos($serviceName, 'pcs') !== false); + $weightValue = $isPcs ? (int)$report->weight : $report->weight; + $alreadyHasUnit = preg_match('/\d+(\.\d+)?\s*(pcs|kg)/i', $serviceName); + @endphp + + @if($alreadyHasUnit) + {!! nl2br(e($serviceName)) !!} + @else + {!! nl2br(e($serviceName)) !!} ({{ $weightValue }} {{ $isPcs ? 'Pcs' : 'Kg' }}) + @endif + + @endif +
+ +
+ @if(strtolower($report->payment_method) == 'qris') + QRIS + @else + TUNAI + @endif + + @if($report->delivery_type == 'none' || !$report->delivery_type) + AMBIL SENDIRI + @else + @php + $typeLabel = "Ambil Sendiri"; + if ($report->delivery_type === 'pickup') $typeLabel = "Pick-up"; + elseif ($report->delivery_type === 'delivery') $typeLabel = "Delivery"; + elseif ($report->delivery_type === 'both') $typeLabel = "Pick-up & Delivery"; + @endphp + {{ $typeLabel }} + @endif +
+ +
+
+ @if(strtolower($report->payment_method) != 'qris') +
+ Uang: Rp {{ number_format($report->cash_received ?? 0, 0, ',', '.') }} + Kembali: Rp {{ number_format($report->cash_change ?? 0, 0, ',', '.') }} +
+ @else + Nominal Pas + @endif + @if($report->delivery_fee > 0) + Ongkir: Rp {{ number_format($report->delivery_fee, 0, ',', '.') }} + @endif +
+
+ Total Pembayaran + Rp {{ number_format($report->total_price, 0, ',', '.') }} +
+
+
+ @empty +
Belum ada transaksi untuk periode ini.
+ @endforelse +
+
+
+
+ + {{-- Script untuk Interaksi --}} + + +
\ No newline at end of file diff --git a/laundry-app/resources/views/settings.blade.php b/laundry-app/resources/views/settings.blade.php new file mode 100644 index 0000000..5d6bcef --- /dev/null +++ b/laundry-app/resources/views/settings.blade.php @@ -0,0 +1,378 @@ + +
+ + + + + + + + {{-- Header Section --}} +
+
+

Pengaturan Sistem

+

Kelola profil toko, lokasi peta, dan daftar akun staf Anda.

+
+
+ +
+ {{-- PENGATURAN TOKO SECTION --}} +
+
+
+ storefront +
+

Pengaturan Toko

+
+ +
+ @csrf + @method('PUT') + +
+ {{-- Kiri: Info Dasar & Lokasi --}} +
+
+ + +
+ +
+ +
+

Geser penanda di peta untuk mengatur titik lokasi toko Anda.

+ +
+
+ + +
+
+ + +
+
+
+
+ + {{-- Kanan: Pengaturan Ongkir & Estimasi --}} +
+
+
+
+

Pengaturan Ongkos Kirim

+

Aktifkan untuk menerapkan tarif berbayar.

+
+ +
+
+
+ + + @error('delivery_fee_per_km') +

{{ $message }}

+ @enderror +
+
+ + +
+
+
+ +
+
+
+

Estimasi Waktu & Layanan

+

Aktifkan layanan cuci Express dan Kilat.

+
+ +
+
+
+ + + @error('express_extra_price') +

{{ $message }}

+ @enderror +
+
+ + + @error('kilat_extra_price') +

{{ $message }}

+ @enderror +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+ badge +
+

Manajemen Akun Pengguna

+
+ + add + Tambah Akun + +
+ + + + {{-- Mobile Card Layout --}} +
+ @forelse($staffs as $staff) +
+
+
+

{{ $staff->name }}

+

{{ $staff->email }}

+
+ + {{ $staff->role }} + +
+
+ + edit Edit + +
+ @csrf + @method('DELETE') + +
+
+
+ @empty +
+ group_off +

Belum ada akun pengguna yang terdaftar.

+
+ @endforelse +
+
+ +
+
+
+
+ logout +
+
+

Keluar Akun

+

Selesaikan sesi aktif Anda di perangkat ini.

+
+
+ +
+ @csrf + +
+
+
+
+
+ + + +
\ No newline at end of file diff --git a/laundry-app/resources/views/staff/create.blade.php b/laundry-app/resources/views/staff/create.blade.php new file mode 100644 index 0000000..bc1652e --- /dev/null +++ b/laundry-app/resources/views/staff/create.blade.php @@ -0,0 +1,67 @@ + +
+
+ + + arrow_back + + +
+

Tambah Pengguna Baru

+

Manajemen Akses Aplikasi

+
+
+ +
+
+
+
+ @csrf + +
+ + + @error('name')

{{ $message }}

@enderror +
+ +
+ + + @error('email')

{{ $message }}

@enderror +
+ +
+ + + @error('password')

{{ $message }}

@enderror +
+ +
+ + + @error('role')

{{ $message }}

@enderror +
+ +
+ + Batal + + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/laundry-app/resources/views/staff/edit.blade.php b/laundry-app/resources/views/staff/edit.blade.php new file mode 100644 index 0000000..b6b2564 --- /dev/null +++ b/laundry-app/resources/views/staff/edit.blade.php @@ -0,0 +1,69 @@ + +
+
+ + + arrow_back + + +
+

Edit Pengguna

+

Perbarui Data Akun

+
+
+ +
+
+
+
+ @csrf + @method('PUT') + +
+ + + @error('name')

{{ $message }}

@enderror +
+ +
+ + + @error('email')

{{ $message }}

@enderror +
+ +
+ + +

Isi hanya jika ingin mengubah kata sandi.

+ @error('password')

{{ $message }}

@enderror +
+ +
+ + + @error('role')

{{ $message }}

@enderror +
+ +
+ + Batal + + +
+
+
+
+
+
+
diff --git a/laundry-app/resources/views/vendor/mail/html/button.blade.php b/laundry-app/resources/views/vendor/mail/html/button.blade.php new file mode 100644 index 0000000..050e969 --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/html/button.blade.php @@ -0,0 +1,24 @@ +@props([ + 'url', + 'color' => 'primary', + 'align' => 'center', +]) + + + + + diff --git a/laundry-app/resources/views/vendor/mail/html/footer.blade.php b/laundry-app/resources/views/vendor/mail/html/footer.blade.php new file mode 100644 index 0000000..3ff41f8 --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/html/footer.blade.php @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/laundry-app/resources/views/vendor/mail/html/header.blade.php b/laundry-app/resources/views/vendor/mail/html/header.blade.php new file mode 100644 index 0000000..276d8ec --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/html/header.blade.php @@ -0,0 +1,8 @@ +@props(['url']) + + + + + + + diff --git a/laundry-app/resources/views/vendor/mail/html/layout.blade.php b/laundry-app/resources/views/vendor/mail/html/layout.blade.php new file mode 100644 index 0000000..037efe3 --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/html/layout.blade.php @@ -0,0 +1,58 @@ + + + +{{ config('app.name') }} + + + + + +{!! $head ?? '' !!} + + + + + + + + + + diff --git a/laundry-app/resources/views/vendor/mail/html/message.blade.php b/laundry-app/resources/views/vendor/mail/html/message.blade.php new file mode 100644 index 0000000..a16bace --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/html/message.blade.php @@ -0,0 +1,27 @@ + +{{-- Header --}} + + +{{ config('app.name') }} + + + +{{-- Body --}} +{!! $slot !!} + +{{-- Subcopy --}} +@isset($subcopy) + + +{!! $subcopy !!} + + +@endisset + +{{-- Footer --}} + + +ยฉ {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights reserved.') }} + + + diff --git a/laundry-app/resources/views/vendor/mail/html/panel.blade.php b/laundry-app/resources/views/vendor/mail/html/panel.blade.php new file mode 100644 index 0000000..2975a60 --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/html/panel.blade.php @@ -0,0 +1,14 @@ + + + + + + diff --git a/laundry-app/resources/views/vendor/mail/html/subcopy.blade.php b/laundry-app/resources/views/vendor/mail/html/subcopy.blade.php new file mode 100644 index 0000000..790ce6c --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/html/subcopy.blade.php @@ -0,0 +1,7 @@ + + + + + diff --git a/laundry-app/resources/views/vendor/mail/html/table.blade.php b/laundry-app/resources/views/vendor/mail/html/table.blade.php new file mode 100644 index 0000000..a5f3348 --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/html/table.blade.php @@ -0,0 +1,3 @@ +
+{{ Illuminate\Mail\Markdown::parse($slot) }} +
diff --git a/laundry-app/resources/views/vendor/mail/html/themes/default.css b/laundry-app/resources/views/vendor/mail/html/themes/default.css new file mode 100644 index 0000000..80465b2 --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/html/themes/default.css @@ -0,0 +1,297 @@ +/* Base */ + +body, +body *:not(html):not(style):not(br):not(tr):not(code) { + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + position: relative; +} + +body { + -webkit-text-size-adjust: none; + background-color: #ffffff; + color: #52525b; + height: 100%; + line-height: 1.4; + margin: 0; + padding: 0; + width: 100% !important; +} + +p, +ul, +ol, +blockquote { + line-height: 1.4; + text-align: left; +} + +a { + color: #18181b; +} + +a img { + border: none; +} + +/* Typography */ + +h1 { + color: #18181b; + font-size: 18px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +h2 { + font-size: 16px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +h3 { + font-size: 14px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +p { + font-size: 16px; + line-height: 1.5em; + margin-top: 0; + text-align: left; +} + +p.sub { + font-size: 12px; +} + +img { + max-width: 100%; +} + +/* Layout */ + +.wrapper { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + background-color: #fafafa; + margin: 0; + padding: 0; + width: 100%; +} + +.content { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 0; + padding: 0; + width: 100%; +} + +/* Header */ + +.header { + padding: 25px 0; + text-align: center; +} + +.header a { + color: #18181b; + font-size: 19px; + font-weight: bold; + text-decoration: none; +} + +/* Logo */ + +.logo { + height: 75px; + margin-top: 15px; + margin-bottom: 10px; + max-height: 75px; + width: 75px; +} + +/* Body */ + +.body { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + background-color: #fafafa; + border-bottom: 1px solid #fafafa; + border-top: 1px solid #fafafa; + margin: 0; + padding: 0; + width: 100%; +} + +.inner-body { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; + background-color: #ffffff; + border-color: #e4e4e7; + border-radius: 4px; + border-width: 1px; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1); + margin: 0 auto; + padding: 0; + width: 570px; +} + +.inner-body a { + word-break: break-all; +} + +/* Subcopy */ + +.subcopy { + border-top: 1px solid #e4e4e7; + margin-top: 25px; + padding-top: 25px; +} + +.subcopy p { + font-size: 14px; +} + +/* Footer */ + +.footer { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; + margin: 0 auto; + padding: 0; + text-align: center; + width: 570px; +} + +.footer p { + color: #a1a1aa; + font-size: 12px; + text-align: center; +} + +.footer a { + color: #a1a1aa; + text-decoration: underline; +} + +/* Tables */ + +.table table { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 30px auto; + width: 100%; +} + +.table th { + border-bottom: 1px solid #e4e4e7; + margin: 0; + padding-bottom: 8px; +} + +.table td { + color: #52525b; + font-size: 15px; + line-height: 18px; + margin: 0; + padding: 10px 0; +} + +.content-cell { + max-width: 100vw; + padding: 32px; +} + +/* Buttons */ + +.action { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 30px auto; + padding: 0; + text-align: center; + width: 100%; + float: unset; +} + +.button { + -webkit-text-size-adjust: none; + border-radius: 4px; + color: #fff; + display: inline-block; + overflow: hidden; + text-decoration: none; +} + +.button-blue, +.button-primary { + background-color: #18181b; + border-bottom: 8px solid #18181b; + border-left: 18px solid #18181b; + border-right: 18px solid #18181b; + border-top: 8px solid #18181b; +} + +.button-green, +.button-success { + background-color: #16a34a; + border-bottom: 8px solid #16a34a; + border-left: 18px solid #16a34a; + border-right: 18px solid #16a34a; + border-top: 8px solid #16a34a; +} + +.button-red, +.button-error { + background-color: #dc2626; + border-bottom: 8px solid #dc2626; + border-left: 18px solid #dc2626; + border-right: 18px solid #dc2626; + border-top: 8px solid #dc2626; +} + +/* Panels */ + +.panel { + border-left: #18181b solid 4px; + margin: 21px 0; +} + +.panel-content { + background-color: #fafafa; + color: #52525b; + padding: 16px; +} + +.panel-content p { + color: #52525b; +} + +.panel-item { + padding: 0; +} + +.panel-item p:last-of-type { + margin-bottom: 0; + padding-bottom: 0; +} + +/* Utilities */ + +.break-all { + word-break: break-all; +} diff --git a/laundry-app/resources/views/vendor/mail/text/button.blade.php b/laundry-app/resources/views/vendor/mail/text/button.blade.php new file mode 100644 index 0000000..97444eb --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/text/button.blade.php @@ -0,0 +1 @@ +{{ $slot }}: {{ $url }} diff --git a/laundry-app/resources/views/vendor/mail/text/footer.blade.php b/laundry-app/resources/views/vendor/mail/text/footer.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/text/footer.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/laundry-app/resources/views/vendor/mail/text/header.blade.php b/laundry-app/resources/views/vendor/mail/text/header.blade.php new file mode 100644 index 0000000..97444eb --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/text/header.blade.php @@ -0,0 +1 @@ +{{ $slot }}: {{ $url }} diff --git a/laundry-app/resources/views/vendor/mail/text/layout.blade.php b/laundry-app/resources/views/vendor/mail/text/layout.blade.php new file mode 100644 index 0000000..ec58e83 --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/text/layout.blade.php @@ -0,0 +1,9 @@ +{!! strip_tags($header ?? '') !!} + +{!! strip_tags($slot) !!} +@isset($subcopy) + +{!! strip_tags($subcopy) !!} +@endisset + +{!! strip_tags($footer ?? '') !!} diff --git a/laundry-app/resources/views/vendor/mail/text/message.blade.php b/laundry-app/resources/views/vendor/mail/text/message.blade.php new file mode 100644 index 0000000..80bce21 --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/text/message.blade.php @@ -0,0 +1,27 @@ + + {{-- Header --}} + + + {{ config('app.name') }} + + + + {{-- Body --}} + {{ $slot }} + + {{-- Subcopy --}} + @isset($subcopy) + + + {{ $subcopy }} + + + @endisset + + {{-- Footer --}} + + + ยฉ {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') + + + diff --git a/laundry-app/resources/views/vendor/mail/text/panel.blade.php b/laundry-app/resources/views/vendor/mail/text/panel.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/text/panel.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/laundry-app/resources/views/vendor/mail/text/subcopy.blade.php b/laundry-app/resources/views/vendor/mail/text/subcopy.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/text/subcopy.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/laundry-app/resources/views/vendor/mail/text/table.blade.php b/laundry-app/resources/views/vendor/mail/text/table.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/laundry-app/resources/views/vendor/mail/text/table.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/laundry-app/resources/views/vendor/notifications/email.blade.php b/laundry-app/resources/views/vendor/notifications/email.blade.php new file mode 100644 index 0000000..79c2408 --- /dev/null +++ b/laundry-app/resources/views/vendor/notifications/email.blade.php @@ -0,0 +1,58 @@ + +{{-- Greeting --}} +@if (! empty($greeting)) +# {{ $greeting }} +@else +@if ($level === 'error') +# @lang('Whoops!') +@else +# @lang('Hello!') +@endif +@endif + +{{-- Intro Lines --}} +@foreach ($introLines as $line) +{{ $line }} + +@endforeach + +{{-- Action Button --}} +@isset($actionText) + $level, + default => 'primary', + }; +?> + +{{ $actionText }} + +@endisset + +{{-- Outro Lines --}} +@foreach ($outroLines as $line) +{{ $line }} + +@endforeach + +{{-- Salutation --}} +@if (! empty($salutation)) +{{ $salutation }} +@else +@lang('Regards,')
+{{ config('app.name') }} +@endif + +{{-- Subcopy --}} +@isset($actionText) + +@lang( + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n". + 'into your web browser:', + [ + 'actionText' => $actionText, + ] +) [{{ $displayableActionUrl }}]({{ $actionUrl }}) + +@endisset +
diff --git a/laundry-app/routes/api.php b/laundry-app/routes/api.php new file mode 100644 index 0000000..1390ad9 --- /dev/null +++ b/laundry-app/routes/api.php @@ -0,0 +1,45 @@ +group(function () { + + // User Profile (Opsional, untuk cek siapa yang login) + Route::get('/user', function (Request $request) { + return $request->user(); + }); + + // Jasa (Otomatis terfilter per Owner berkat Global Scope) + Route::get('/services', [ServiceController::class, 'index']); + + // Dashboard & Stats + Route::get('/dashboard/stats', [DashboardController::class, 'getStats']); + Route::get('/dashboard/activities', [DashboardController::class, 'getActivities']); + + // Driver & Pickup Tasks (Integrated into TransactionController) + Route::post('/pickups', [TransactionController::class, 'requestPickup']); + Route::get('/driver/tasks', [TransactionController::class, 'getDriverTasks']); + Route::put('/pickups/{id}/status', [TransactionController::class, 'updateStatus']); + Route::get('/pickups/arrived', [TransactionController::class, 'getOrdersByStatus'])->defaults('status', 'JEMPUTAN TIBA'); + + // Transaksi & Order + Route::get('/transactions', [TransactionController::class, 'index']); + Route::post('/transactions', [TransactionController::class, 'store']); + Route::get('/orders/status/{status}', [TransactionController::class, 'getOrdersByStatus']); + Route::put('/orders/{id}/update-status', [TransactionController::class, 'updateStatus']); + Route::post('/send-whatsapp', [TransactionController::class, 'sendManualWhatsApp']); + Route::post('/check-delivery-fee', [TransactionController::class, 'checkDeliveryFee']); + + // Pengaturan Toko (diakses oleh Staf dari aplikasi mobile) + Route::get('/shop/settings', [\App\Http\Controllers\Api\ShopSettingsController::class, 'getSettings']); + Route::put('/shop/settings', [\App\Http\Controllers\Api\ShopSettingsController::class, 'updateSettings']); +}); diff --git a/laundry-app/routes/auth.php b/laundry-app/routes/auth.php new file mode 100644 index 0000000..658523e --- /dev/null +++ b/laundry-app/routes/auth.php @@ -0,0 +1,59 @@ +group(function () { + Route::get('register', [RegisteredUserController::class, 'create']) + ->name('register'); + + Route::post('register', [RegisteredUserController::class, 'store']); + + Route::get('login', [AuthenticatedSessionController::class, 'create']) + ->name('login'); + + Route::post('login', [AuthenticatedSessionController::class, 'store']); + + Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) + ->name('password.request'); + + Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) + ->name('password.email'); + + Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) + ->name('password.reset'); + + Route::post('reset-password', [NewPasswordController::class, 'store']) + ->name('password.store'); +}); + +Route::middleware('auth')->group(function () { + Route::get('verify-email', EmailVerificationPromptController::class) + ->name('verification.notice'); + + Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) + ->middleware(['signed', 'throttle:6,1']) + ->name('verification.verify'); + + Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) + ->middleware('throttle:6,1') + ->name('verification.send'); + + Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) + ->name('password.confirm'); + + Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); + + Route::put('password', [PasswordController::class, 'update'])->name('password.update'); + + Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) + ->name('logout'); +}); diff --git a/laundry-app/routes/console.php b/laundry-app/routes/console.php new file mode 100644 index 0000000..3c9adf1 --- /dev/null +++ b/laundry-app/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/laundry-app/routes/web.php b/laundry-app/routes/web.php new file mode 100644 index 0000000..8f06c34 --- /dev/null +++ b/laundry-app/routes/web.php @@ -0,0 +1,55 @@ +route('login'); +}); + +// 3. Route yang memerlukan Login DAN harus ROLE OWNER serta pencegahan cache +Route::middleware(['auth', 'verified', 'owner', 'prevent-back-history'])->group(function () { + + // Dashboard + Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); + Route::get('/orders/status/{status}', [OrderListController::class, 'showByStatus'])->name('orders.by_status'); + Route::delete('/orders/bulk-delete', [OrderListController::class, 'bulkDelete'])->name('orders.bulkDelete'); + + // --- MANAJEMEN LAUNDRY --- + Route::get('/laundry', [LaundryController::class, 'index'])->name('laundry'); + Route::get('/laundry/create', [LaundryController::class, 'create'])->name('laundry.create'); + Route::post('/laundry', [LaundryController::class, 'store'])->name('laundry.store'); + Route::get('/laundry/{id}/edit', [LaundryController::class, 'edit'])->name('laundry.edit'); + Route::put('/laundry/{id}', [LaundryController::class, 'update'])->name('laundry.update'); + Route::delete('/laundry/{id}', [LaundryController::class, 'destroy'])->name('laundry.destroy'); + Route::post('/laundry/status/{id}', [LaundryController::class, 'updateStatus'])->name('laundry.updateStatus'); + + // --- LAPORAN PENDAPATAN --- + Route::get('/reports', [ReportController::class, 'index'])->name('reports.index'); + Route::get('/reports/export', [ReportController::class, 'export'])->name('reports.export'); + Route::post('/reports/export-bulk', [ReportController::class, 'exportBulk'])->name('reports.exportBulk'); + Route::delete('/reports/bulk-delete', [ReportController::class, 'bulkDelete'])->name('reports.bulkDelete'); + + // --- MANAJEMEN STAF & PENGATURAN --- + Route::get('/settings', [UserController::class, 'settings'])->name('settings'); + Route::put('/settings/shop', [UserController::class, 'updateShopSettings'])->name('shop.settings.update'); + Route::get('/staff/create', [StaffController::class, 'create'])->name('staff.create'); + Route::post('/staff/store', [StaffController::class, 'store'])->name('staff.store'); + Route::get('/staff/{id}/edit', [StaffController::class, 'edit'])->name('staff.edit'); + Route::put('/staff/{id}', [StaffController::class, 'update'])->name('staff.update'); + Route::delete('/staff/{id}', [StaffController::class, 'destroy'])->name('staff.destroy'); + + // Profile + Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); + Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); + Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); +}); + +require __DIR__ . '/auth.php'; \ No newline at end of file diff --git a/laundry-app/storage/app/.gitignore b/laundry-app/storage/app/.gitignore new file mode 100644 index 0000000..fedb287 --- /dev/null +++ b/laundry-app/storage/app/.gitignore @@ -0,0 +1,4 @@ +* +!private/ +!public/ +!.gitignore diff --git a/laundry-app/storage/app/private/.gitignore b/laundry-app/storage/app/private/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/laundry-app/storage/app/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/laundry-app/storage/app/public/.gitignore b/laundry-app/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/laundry-app/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/laundry-app/storage/framework/.gitignore b/laundry-app/storage/framework/.gitignore new file mode 100644 index 0000000..05c4471 --- /dev/null +++ b/laundry-app/storage/framework/.gitignore @@ -0,0 +1,9 @@ +compiled.php +config.php +down +events.scanned.php +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/laundry-app/storage/framework/cache/.gitignore b/laundry-app/storage/framework/cache/.gitignore new file mode 100644 index 0000000..01e4a6c --- /dev/null +++ b/laundry-app/storage/framework/cache/.gitignore @@ -0,0 +1,3 @@ +* +!data/ +!.gitignore diff --git a/laundry-app/storage/framework/cache/data/.gitignore b/laundry-app/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/laundry-app/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/laundry-app/storage/framework/sessions/.gitignore b/laundry-app/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/laundry-app/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/laundry-app/storage/framework/testing/.gitignore b/laundry-app/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/laundry-app/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/laundry-app/storage/framework/views/.gitignore b/laundry-app/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/laundry-app/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/laundry-app/storage/logs/.gitignore b/laundry-app/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/laundry-app/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/laundry-app/tailwind.config.js b/laundry-app/tailwind.config.js new file mode 100644 index 0000000..c29eb1a --- /dev/null +++ b/laundry-app/tailwind.config.js @@ -0,0 +1,21 @@ +import defaultTheme from 'tailwindcss/defaultTheme'; +import forms from '@tailwindcss/forms'; + +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', + './storage/framework/views/*.php', + './resources/views/**/*.blade.php', + ], + + theme: { + extend: { + fontFamily: { + sans: ['Figtree', ...defaultTheme.fontFamily.sans], + }, + }, + }, + + plugins: [forms], +}; diff --git a/laundry-app/tests/Feature/Auth/AuthenticationTest.php b/laundry-app/tests/Feature/Auth/AuthenticationTest.php new file mode 100644 index 0000000..13dcb7c --- /dev/null +++ b/laundry-app/tests/Feature/Auth/AuthenticationTest.php @@ -0,0 +1,54 @@ +get('/login'); + + $response->assertStatus(200); + } + + public function test_users_can_authenticate_using_the_login_screen(): void + { + $user = User::factory()->create(); + + $response = $this->post('/login', [ + 'email' => $user->email, + 'password' => 'password', + ]); + + $this->assertAuthenticated(); + $response->assertRedirect(route('dashboard', absolute: false)); + } + + public function test_users_can_not_authenticate_with_invalid_password(): void + { + $user = User::factory()->create(); + + $this->post('/login', [ + 'email' => $user->email, + 'password' => 'wrong-password', + ]); + + $this->assertGuest(); + } + + public function test_users_can_logout(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/logout'); + + $this->assertGuest(); + $response->assertRedirect('/'); + } +} diff --git a/laundry-app/tests/Feature/Auth/EmailVerificationTest.php b/laundry-app/tests/Feature/Auth/EmailVerificationTest.php new file mode 100644 index 0000000..705570b --- /dev/null +++ b/laundry-app/tests/Feature/Auth/EmailVerificationTest.php @@ -0,0 +1,58 @@ +unverified()->create(); + + $response = $this->actingAs($user)->get('/verify-email'); + + $response->assertStatus(200); + } + + public function test_email_can_be_verified(): void + { + $user = User::factory()->unverified()->create(); + + Event::fake(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1($user->email)] + ); + + $response = $this->actingAs($user)->get($verificationUrl); + + Event::assertDispatched(Verified::class); + $this->assertTrue($user->fresh()->hasVerifiedEmail()); + $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); + } + + public function test_email_is_not_verified_with_invalid_hash(): void + { + $user = User::factory()->unverified()->create(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1('wrong-email')] + ); + + $this->actingAs($user)->get($verificationUrl); + + $this->assertFalse($user->fresh()->hasVerifiedEmail()); + } +} diff --git a/laundry-app/tests/Feature/Auth/PasswordConfirmationTest.php b/laundry-app/tests/Feature/Auth/PasswordConfirmationTest.php new file mode 100644 index 0000000..ff85721 --- /dev/null +++ b/laundry-app/tests/Feature/Auth/PasswordConfirmationTest.php @@ -0,0 +1,44 @@ +create(); + + $response = $this->actingAs($user)->get('/confirm-password'); + + $response->assertStatus(200); + } + + public function test_password_can_be_confirmed(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/confirm-password', [ + 'password' => 'password', + ]); + + $response->assertRedirect(); + $response->assertSessionHasNoErrors(); + } + + public function test_password_is_not_confirmed_with_invalid_password(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/confirm-password', [ + 'password' => 'wrong-password', + ]); + + $response->assertSessionHasErrors(); + } +} diff --git a/laundry-app/tests/Feature/Auth/PasswordResetTest.php b/laundry-app/tests/Feature/Auth/PasswordResetTest.php new file mode 100644 index 0000000..aa50350 --- /dev/null +++ b/laundry-app/tests/Feature/Auth/PasswordResetTest.php @@ -0,0 +1,73 @@ +get('/forgot-password'); + + $response->assertStatus(200); + } + + public function test_reset_password_link_can_be_requested(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class); + } + + public function test_reset_password_screen_can_be_rendered(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) { + $response = $this->get('/reset-password/'.$notification->token); + + $response->assertStatus(200); + + return true; + }); + } + + public function test_password_can_be_reset_with_valid_token(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { + $response = $this->post('/reset-password', [ + 'token' => $notification->token, + 'email' => $user->email, + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('login')); + + return true; + }); + } +} diff --git a/laundry-app/tests/Feature/Auth/PasswordUpdateTest.php b/laundry-app/tests/Feature/Auth/PasswordUpdateTest.php new file mode 100644 index 0000000..ca28c6c --- /dev/null +++ b/laundry-app/tests/Feature/Auth/PasswordUpdateTest.php @@ -0,0 +1,51 @@ +create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->put('/password', [ + 'current_password' => 'password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); + } + + public function test_correct_password_must_be_provided_to_update_password(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->put('/password', [ + 'current_password' => 'wrong-password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasErrorsIn('updatePassword', 'current_password') + ->assertRedirect('/profile'); + } +} diff --git a/laundry-app/tests/Feature/Auth/RegistrationTest.php b/laundry-app/tests/Feature/Auth/RegistrationTest.php new file mode 100644 index 0000000..1489d0e --- /dev/null +++ b/laundry-app/tests/Feature/Auth/RegistrationTest.php @@ -0,0 +1,31 @@ +get('/register'); + + $response->assertStatus(200); + } + + public function test_new_users_can_register(): void + { + $response = $this->post('/register', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $this->assertAuthenticated(); + $response->assertRedirect(route('dashboard', absolute: false)); + } +} diff --git a/laundry-app/tests/Feature/ExampleTest.php b/laundry-app/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..bd5d6f1 --- /dev/null +++ b/laundry-app/tests/Feature/ExampleTest.php @@ -0,0 +1,20 @@ +get('/'); + + $response->assertStatus(302); + } + +} diff --git a/laundry-app/tests/Feature/ProfileTest.php b/laundry-app/tests/Feature/ProfileTest.php new file mode 100644 index 0000000..262a1d8 --- /dev/null +++ b/laundry-app/tests/Feature/ProfileTest.php @@ -0,0 +1,100 @@ +create(['role' => 'owner']); + + $response = $this + ->actingAs($user) + ->get('/profile'); + + $response->assertOk(); + } + + public function test_profile_information_can_be_updated(): void + { + $user = User::factory()->create(['role' => 'owner']); + + + $response = $this + ->actingAs($user) + ->patch('/profile', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $user->refresh(); + + $this->assertSame('Test User', $user->name); + $this->assertSame('test@example.com', $user->email); + $this->assertNull($user->email_verified_at); + } + + public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void + { + $user = User::factory()->create(['role' => 'owner']); + + $response = $this + ->actingAs($user) + ->patch('/profile', [ + 'name' => 'Test User', + 'email' => $user->email, + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $this->assertNotNull($user->refresh()->email_verified_at); + } + + public function test_user_can_delete_their_account(): void + { + $user = User::factory()->create(['role' => 'owner']); + + $response = $this + ->actingAs($user) + ->delete('/profile', [ + 'password' => 'password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/'); + + $this->assertGuest(); + $this->assertNull($user->fresh()); + } + + public function test_correct_password_must_be_provided_to_delete_account(): void + { + $user = User::factory()->create(['role' => 'owner']); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->delete('/profile', [ + 'password' => 'wrong-password', + ]); + + $response + ->assertSessionHasErrorsIn('userDeletion', 'password') + ->assertRedirect('/profile'); + + $this->assertNotNull($user->fresh()); + } +} diff --git a/laundry-app/tests/TestCase.php b/laundry-app/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/laundry-app/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +} diff --git a/laundry-app/vite.config.js b/laundry-app/vite.config.js new file mode 100644 index 0000000..421b569 --- /dev/null +++ b/laundry-app/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + }), + ], +}); diff --git a/laundry_mobile/.gitignore b/laundry_mobile/.gitignore new file mode 100644 index 0000000..29a3a50 --- /dev/null +++ b/laundry_mobile/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/laundry_mobile/.metadata b/laundry_mobile/.metadata new file mode 100644 index 0000000..d044da8 --- /dev/null +++ b/laundry_mobile/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + - platform: android + create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + - platform: ios + create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + - platform: linux + create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + - platform: macos + create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + - platform: web + create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + - platform: windows + create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/laundry_mobile/README.md b/laundry_mobile/README.md new file mode 100644 index 0000000..fb487cc --- /dev/null +++ b/laundry_mobile/README.md @@ -0,0 +1,16 @@ +# laundry_mobile + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/laundry_mobile/analysis_options.yaml b/laundry_mobile/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/laundry_mobile/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/laundry_mobile/analyze_machine.json b/laundry_mobile/analyze_machine.json new file mode 100644 index 0000000..37e6cd7 Binary files /dev/null and b/laundry_mobile/analyze_machine.json differ diff --git a/laundry_mobile/android/.gitignore b/laundry_mobile/android/.gitignore new file mode 100644 index 0000000..55afd91 --- /dev/null +++ b/laundry_mobile/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/laundry_mobile/android/app/build.gradle b/laundry_mobile/android/app/build.gradle new file mode 100644 index 0000000..44da1c3 --- /dev/null +++ b/laundry_mobile/android/app/build.gradle @@ -0,0 +1,46 @@ +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" + id "com.google.gms.google-services" +} + +android { + namespace = "com.example.laundry_mobile" + compileSdkVersion 36 + ndkVersion = "26.1.10909125" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.laundry_mobile" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdkVersion 23 + targetSdkVersion 36 + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.debug + } + } +} + +flutter { + source = "../.." +} + diff --git a/laundry_mobile/android/app/google-services.json b/laundry_mobile/android/app/google-services.json new file mode 100644 index 0000000..1f9e228 --- /dev/null +++ b/laundry_mobile/android/app/google-services.json @@ -0,0 +1,30 @@ +{ + "project_info": { + "project_number": "735903066829", + "firebase_url": "https://mylaundry-d02fb-default-rtdb.asia-southeast1.firebasedatabase.app", + "project_id": "mylaundry-d02fb", + "storage_bucket": "mylaundry-d02fb.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:735903066829:android:10c85ce58e5069323c1784", + "android_client_info": { + "package_name": "com.example.laundry_mobile" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyD3Up1sdJB9tmO2Zw2eR8z-obOWGGzgjd0" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/laundry_mobile/android/app/src/debug/AndroidManifest.xml b/laundry_mobile/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/laundry_mobile/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/laundry_mobile/android/app/src/main/AndroidManifest.xml b/laundry_mobile/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5c237ae --- /dev/null +++ b/laundry_mobile/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/laundry_mobile/android/app/src/main/kotlin/com/example/laundry_mobile/MainActivity.kt b/laundry_mobile/android/app/src/main/kotlin/com/example/laundry_mobile/MainActivity.kt new file mode 100644 index 0000000..4ff8e6d --- /dev/null +++ b/laundry_mobile/android/app/src/main/kotlin/com/example/laundry_mobile/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.laundry_mobile + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/laundry_mobile/android/app/src/main/res/drawable-v21/launch_background.xml b/laundry_mobile/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/laundry_mobile/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/laundry_mobile/android/app/src/main/res/drawable/launch_background.xml b/laundry_mobile/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/laundry_mobile/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/laundry_mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/laundry_mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/laundry_mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/laundry_mobile/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/laundry_mobile/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 0000000..72c9a6f Binary files /dev/null and b/laundry_mobile/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/laundry_mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/laundry_mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/laundry_mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/laundry_mobile/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/laundry_mobile/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 0000000..fd40cdd Binary files /dev/null and b/laundry_mobile/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/laundry_mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/laundry_mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/laundry_mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/laundry_mobile/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/laundry_mobile/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 0000000..8b6700a Binary files /dev/null and b/laundry_mobile/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/laundry_mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/laundry_mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/laundry_mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/laundry_mobile/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/laundry_mobile/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 0000000..bfcb837 Binary files /dev/null and b/laundry_mobile/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/laundry_mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/laundry_mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/laundry_mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/laundry_mobile/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/laundry_mobile/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 0000000..2d0dc75 Binary files /dev/null and b/laundry_mobile/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/laundry_mobile/android/app/src/main/res/values-night/styles.xml b/laundry_mobile/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/laundry_mobile/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/laundry_mobile/android/app/src/main/res/values/styles.xml b/laundry_mobile/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/laundry_mobile/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/laundry_mobile/android/app/src/profile/AndroidManifest.xml b/laundry_mobile/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/laundry_mobile/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/laundry_mobile/android/build.gradle b/laundry_mobile/android/build.gradle new file mode 100644 index 0000000..b2a9909 --- /dev/null +++ b/laundry_mobile/android/build.gradle @@ -0,0 +1,32 @@ + + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.ext.flutter = [ + compileSdkVersion: 36, + targetSdkVersion: 36, + minSdkVersion: 23, +] + +ext { + compileSdkVersion = 36 + targetSdkVersion = 36 + minSdkVersion = 23 +} + +rootProject.buildDir = "../build" +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/laundry_mobile/android/gradle.properties b/laundry_mobile/android/gradle.properties new file mode 100644 index 0000000..86c765c --- /dev/null +++ b/laundry_mobile/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true +kotlin.incremental=false diff --git a/laundry_mobile/android/gradle/wrapper/gradle-wrapper.properties b/laundry_mobile/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..afa1e8e --- /dev/null +++ b/laundry_mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/laundry_mobile/android/settings.gradle b/laundry_mobile/android/settings.gradle new file mode 100644 index 0000000..9425689 --- /dev/null +++ b/laundry_mobile/android/settings.gradle @@ -0,0 +1,26 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.6.0" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false + id "com.google.gms.google-services" version "4.4.1" apply false +} + +include ":app" diff --git a/laundry_mobile/assets/app_icon.png b/laundry_mobile/assets/app_icon.png new file mode 100644 index 0000000..8109f2c Binary files /dev/null and b/laundry_mobile/assets/app_icon.png differ diff --git a/laundry_mobile/assets/beranda.jpg b/laundry_mobile/assets/beranda.jpg new file mode 100644 index 0000000..0f5acbf Binary files /dev/null and b/laundry_mobile/assets/beranda.jpg differ diff --git a/laundry_mobile/assets/qris.png b/laundry_mobile/assets/qris.png new file mode 100644 index 0000000..dff138e Binary files /dev/null and b/laundry_mobile/assets/qris.png differ diff --git a/laundry_mobile/ios/.gitignore b/laundry_mobile/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/laundry_mobile/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/laundry_mobile/ios/Flutter/AppFrameworkInfo.plist b/laundry_mobile/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/laundry_mobile/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/laundry_mobile/ios/Flutter/Debug.xcconfig b/laundry_mobile/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/laundry_mobile/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/laundry_mobile/ios/Flutter/Release.xcconfig b/laundry_mobile/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/laundry_mobile/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/laundry_mobile/ios/Runner.xcodeproj/project.pbxproj b/laundry_mobile/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..ed7b0af --- /dev/null +++ b/laundry_mobile/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.laundryMobile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.laundryMobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.laundryMobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.laundryMobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.laundryMobile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.laundryMobile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/laundry_mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/laundry_mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/laundry_mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/laundry_mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/laundry_mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/laundry_mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/laundry_mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/laundry_mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/laundry_mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/laundry_mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/laundry_mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..8e3ca5d --- /dev/null +++ b/laundry_mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/laundry_mobile/ios/Runner.xcworkspace/contents.xcworkspacedata b/laundry_mobile/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/laundry_mobile/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/laundry_mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/laundry_mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/laundry_mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/laundry_mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/laundry_mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/laundry_mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/laundry_mobile/ios/Runner/AppDelegate.swift b/laundry_mobile/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/laundry_mobile/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..5acfb7c Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..6b8ccfd Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..57807bc Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..eae5c2f Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4934cea Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..06e511b Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..2727ad8 Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..57807bc Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..6567b09 Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..fb7cf1d Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..4756a38 Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..fc110c4 Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..1333c42 Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..57cb71c Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..fb7cf1d Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..736fb48 Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..72c9a6f Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..bfcb837 Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..5a79227 Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..9fb2dd2 Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..49def81 Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/laundry_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/laundry_mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard b/laundry_mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/laundry_mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/laundry_mobile/ios/Runner/Base.lproj/Main.storyboard b/laundry_mobile/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/laundry_mobile/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/laundry_mobile/ios/Runner/Info.plist b/laundry_mobile/ios/Runner/Info.plist new file mode 100644 index 0000000..7c9fcd3 --- /dev/null +++ b/laundry_mobile/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + MyLaundry + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + MyLaundry + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/laundry_mobile/ios/Runner/Runner-Bridging-Header.h b/laundry_mobile/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/laundry_mobile/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/laundry_mobile/ios/RunnerTests/RunnerTests.swift b/laundry_mobile/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/laundry_mobile/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/laundry_mobile/lib/config.dart b/laundry_mobile/lib/config.dart new file mode 100644 index 0000000..86ef13c --- /dev/null +++ b/laundry_mobile/lib/config.dart @@ -0,0 +1,23 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class AppConfig { + static String _ip = "192.168.1.4"; // IP Default + static String _port = "8000"; + + static String get baseUrl => "http://$_ip:$_port/api"; + static String get rawIp => _ip; + + // Memuat IP dari penyimpanan HP + static Future loadConfig() async { + final prefs = await SharedPreferences.getInstance(); + _ip = prefs.getString('server_ip') ?? _ip; + _port = prefs.getString('server_port') ?? _port; + } + + // Menyimpan IP baru ke HP + static Future setIp(String newIp) async { + _ip = newIp; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('server_ip', newIp); + } +} diff --git a/laundry_mobile/lib/driver/beranda.dart b/laundry_mobile/lib/driver/beranda.dart new file mode 100644 index 0000000..ce0f51f --- /dev/null +++ b/laundry_mobile/lib/driver/beranda.dart @@ -0,0 +1,1055 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'dart:async'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../config.dart'; + +import 'profile.dart'; +import '../staf/home/payments.dart'; + +class BerandaPage extends StatefulWidget { + final Map userData; + const BerandaPage({super.key, required this.userData}); + + @override + State createState() => _BerandaPageState(); +} + +class _BerandaPageState extends State { + int _selectedIndex = 0; + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF7F9FB), // Matches staff background + body: IndexedStack( + index: _selectedIndex, + children: [ + HomeDriverTab(userData: widget.userData), + ProfilePage(userData: widget.userData), + ], + ), + bottomNavigationBar: _buildPersistentBottomNav(), + ); + } + + Widget _buildPersistentBottomNav() { + return Container( + height: 64 + MediaQuery.of(context).padding.bottom, + decoration: BoxDecoration( + color: const Color(0xFFFFFFFF).withOpacity(0.95), + border: + const Border(top: BorderSide(color: Color(0xFFE6E8EA), width: 1.0)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, -1)) + ], + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _navItem(Icons.home, "Beranda", _selectedIndex == 0, () { + setState(() => _selectedIndex = 0); + }), + _navItem(Icons.person, "Profil", _selectedIndex == 1, () { + setState(() => _selectedIndex = 1); + }), + ], + ), + ), + ), + ); + } + + Widget _navItem( + IconData icon, String label, bool isActive, VoidCallback onTap) { + return InkWell( + onTap: onTap, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + child: Container( + width: 64, + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 24, + color: isActive + ? const Color(0xFF0058BE) + : const Color(0xFF424754).withOpacity(0.6), + ), + const SizedBox(height: 2), + Text( + label, + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: isActive ? FontWeight.w700 : FontWeight.w500, + color: isActive + ? const Color(0xFF0058BE) + : const Color(0xFF424754).withOpacity(0.6), + ), + ), + ], + ), + ), + ); + } +} + +class HomeDriverTab extends StatefulWidget { + final Map userData; + const HomeDriverTab({super.key, required this.userData}); + + @override + State createState() => _HomeDriverTabState(); +} + +class _HomeDriverTabState extends State + with SingleTickerProviderStateMixin { + final Color bgLight = const Color(0xFFF7F9FB); + final Color surfaceColor = const Color(0xFFFFFFFF); + final Color primaryColor = const Color(0xFF0058BE); + final Color primaryDark = const Color(0xFF004395); + final Color textDark = const Color(0xFF191C1E); + final Color textGray = const Color(0xFF424754); + final Color borderLight = const Color(0xFFE6E8EA); + + final Color primary = const Color(0xFF0058BE); + final Color onPrimary = const Color(0xFFFFFFFF); + final Color primaryContainer = const Color(0xFF2170E4); + final Color surfaceContainerLowest = const Color(0xFFFFFFFF); + final Color surfaceContainerLow = const Color(0xFFF2F4F6); + final Color surfaceContainerHigh = const Color(0xFFE6E8EA); + final Color onSurface = const Color(0xFF191C1E); + final Color onSurfaceVariant = const Color(0xFF424754); + final Color outlineVariant = const Color(0xFFC2C6D6); + final Color outline = const Color(0xFF727785); + final Color secondaryContainer = const Color(0xFFC3ECD7); + final Color onSecondaryContainer = const Color(0xFF476C5B); + + List tasks = []; + Set _sentMessages = {}; + bool isLoading = true; + late TabController _tabController; + final Set _submittingTaskIds = {}; + final Set _sendingWhatsAppTaskIds = {}; + + String formatHarga(dynamic value) { + if (value == null) return "0"; + String val = value.toString(); + RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'); + return val.replaceAllMapped(reg, (Match m) => '${m[1]}.'); + } + + Widget _buildPaymentBadge(Map task) { + final payMethod = (task['payment_method'] ?? "").toString().toUpperCase(); + final cashRec = num.tryParse(task['cash_received']?.toString() ?? "0") ?? 0; + final isUnpaid = payMethod == "BAYAR NANTI" && cashRec == 0; + + if (isUnpaid) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFFFFDAD6), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: const Color(0xFF93000A).withOpacity(0.2)), + ), + child: Text( + "BELUM BAYAR", + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.bold, + color: const Color(0xFF93000A), + ), + ), + ); + } else { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: secondaryContainer, + borderRadius: BorderRadius.circular(999), + border: Border.all(color: onSecondaryContainer.withOpacity(0.2)), + ), + child: Text( + "LUNAS", + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.bold, + color: onSecondaryContainer, + ), + ), + ); + } + } + + Timer? _refreshTimer; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + _loadSentMessages(); + _fetchTasks(); + + _refreshTimer = Timer.periodic(const Duration(seconds: 3), (_) { + _fetchTasks(showLoading: false); + }); + } + + @override + void dispose() { + _refreshTimer?.cancel(); + _tabController.dispose(); + super.dispose(); + } + + // Mengambil data pesan terkirim dari memori HP + Future _loadSentMessages() async { + try { + final prefs = await SharedPreferences.getInstance(); + final List? sentList = prefs.getStringList('sent_wa_messages_v2'); + if (sentList != null) { + setState(() { + _sentMessages = sentList.toSet(); + }); + } + } catch (e) { + // ignore: empty_catches + } + } + + // Menyimpan data pesan terkirim ke memori HP + Future _saveSentMessage(String taskId) async { + try { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _sentMessages.add(taskId); + }); + await prefs.setStringList('sent_wa_messages_v2', _sentMessages.toList()); + } catch (e) { + // ignore: empty_catches + } + } + + Future _fetchTasks({bool showLoading = true}) async { + if (!mounted) return; + if (showLoading) setState(() => isLoading = true); + try { + final String? token = widget.userData['access_token']; + final response = await http.get( + Uri.parse('${AppConfig.baseUrl}/driver/tasks'), + headers: { + 'Authorization': 'Bearer $token', + 'Accept': 'application/json', + }, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + if (mounted) { + setState(() { + tasks = data['data'] ?? []; + if (showLoading) isLoading = false; + }); + } + } else { + if (mounted && showLoading) setState(() => isLoading = false); + } + } catch (e) { + if (mounted && showLoading) setState(() => isLoading = false); + } + } + + void _showSnackBar(String msg, Color bg) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Icon( + bg == const Color(0xFF10B981) + ? Icons.check_circle_outline + : Icons.error_outline_rounded, + color: Colors.white, + size: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + bg == const Color(0xFF10B981) ? "Berhasil" : "Peringatan", + style: GoogleFonts.inter( + fontWeight: FontWeight.w800, + fontSize: 14, + color: Colors.white), + ), + Text( + msg, + style: GoogleFonts.inter( + fontSize: 12, + color: Colors.white.withOpacity(0.9), + fontWeight: FontWeight.w500), + ), + ], + ), + ), + ], + ), + ), + behavior: SnackBarBehavior.floating, + backgroundColor: bg.withOpacity(0.95), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: Colors.white.withOpacity(0.2), width: 1), + ), + duration: const Duration(seconds: 3), + )); + } + + Future _updateTaskStatus(Map task) async { + final String taskId = task['type_id'] ?? ''; + if (_submittingTaskIds.contains(taskId)) return; + + setState(() { + _submittingTaskIds.add(taskId); + }); + + final String? token = widget.userData['access_token']; + final id = task['type_id'].substring(1); + final isJemput = task['task_type'] == 'jemput'; + final url = isJemput + ? '${AppConfig.baseUrl}/pickups/$id/status' + : '${AppConfig.baseUrl}/orders/$id/update-status'; + + try { + final response = await http.put( + Uri.parse(url), + headers: { + 'Authorization': 'Bearer $token', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: json.encode({'status': isJemput ? 'SELESAI' : 'DITERIMA'}), + ); + + if (response.statusCode == 200) { + if (mounted) { + _showSnackBar( + "Tugas berhasil diselesaikan!", const Color(0xFF10B981)); + _fetchTasks(); + } + } + } catch (e) { + // ignore: empty_catches + } finally { + if (mounted) { + setState(() { + _submittingTaskIds.remove(taskId); + }); + } + } + } + + Future _openGoogleMaps(String address, String lat, String lng) async { + Uri navUri = (lat.isNotEmpty && lng.isNotEmpty && lat != '0.0') + ? Uri.parse('google.navigation:q=$lat,$lng') + : Uri.parse('google.navigation:q=${Uri.encodeComponent(address)}'); + + if (await canLaunchUrl(navUri)) { + await launchUrl(navUri); + } else { + // Fallback jika tidak ada aplikasi Google Maps terinstall + Uri webUri = (lat.isNotEmpty && lng.isNotEmpty && lat != '0.0') + ? Uri.parse( + 'https://www.google.com/maps/dir/?api=1&destination=$lat,$lng') + : Uri.parse( + 'https://www.google.com/maps/search/?api=1&query=${Uri.encodeComponent(address)}'); + if (await canLaunchUrl(webUri)) { + await launchUrl(webUri, mode: LaunchMode.externalApplication); + } + } + } + + Future _sendWhatsApp(String phone, String customerName, + String orderNumber, String taskType, String taskId) async { + if (_sendingWhatsAppTaskIds.contains(taskId)) return; + + setState(() { + _sendingWhatsAppTaskIds.add(taskId); + }); + + final String? token = widget.userData['access_token']; + try { + final response = await http.post( + Uri.parse('${AppConfig.baseUrl}/send-whatsapp'), + headers: { + 'Authorization': 'Bearer $token', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: json.encode({ + 'phone': phone, + 'customer_name': customerName, + 'task_type': taskType, + }), + ); + + if (response.statusCode == 200) { + if (mounted) { + _saveSentMessage(taskId); + _showSnackBar("Pesan WhatsApp terkirim", const Color(0xFF10B981)); + } + } + } catch (e) { + // ignore: empty_catches + } finally { + if (mounted) { + setState(() { + _sendingWhatsAppTaskIds.remove(taskId); + }); + } + } + } + + @override + Widget build(BuildContext context) { + String userName = widget.userData['user']?['name'] ?? "Driver"; + List pickupTasks = + tasks.where((t) => t['task_type'] == 'jemput').toList(); + List deliveryTasks = + tasks.where((t) => t['task_type'] == 'antar').toList(); + + return SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Halo, $userName!", + style: GoogleFonts.inter( + fontSize: 24, + fontWeight: FontWeight.w600, + color: onSurface, + letterSpacing: -0.5)), + const SizedBox(height: 4), + Text("Siap bertugas hari ini?", + style: GoogleFonts.inter( + fontSize: 13, + color: onSurfaceVariant, + fontWeight: FontWeight.w500)), + ], + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Container( + height: 48, + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: surfaceContainerHigh, + borderRadius: BorderRadius.circular(999), + ), + child: TabBar( + controller: _tabController, + indicator: BoxDecoration( + borderRadius: BorderRadius.circular(999), + color: primary, + boxShadow: [ + BoxShadow( + color: primary.withOpacity(0.2), + blurRadius: 12, + offset: const Offset(0, 4)) + ], + ), + labelColor: Colors.white, + unselectedLabelColor: onSurfaceVariant, + labelStyle: GoogleFonts.inter( + fontWeight: FontWeight.w500, fontSize: 13), + unselectedLabelStyle: GoogleFonts.inter( + fontWeight: FontWeight.w500, fontSize: 13), + indicatorSize: TabBarIndicatorSize.tab, + dividerColor: Colors.transparent, + splashBorderRadius: BorderRadius.circular(999), + tabs: [ + Tab(text: "Pick-up (${pickupTasks.length})"), + Tab(text: "Delivery (${deliveryTasks.length})"), + ], + ), + ), + ), + const SizedBox(height: 16), + Expanded( + child: isLoading + ? const Center(child: CircularProgressIndicator()) + : TabBarView( + controller: _tabController, + children: [ + _buildTaskList(pickupTasks, "Tidak ada tugas pick up"), + _buildTaskList(deliveryTasks, "Tidak ada tugas antar"), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTaskList(List list, String emptyMessage) { + if (list.isEmpty) { + return RefreshIndicator( + onRefresh: _fetchTasks, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Container( + height: MediaQuery.of(context).size.height * 0.5, + width: double.infinity, + padding: const EdgeInsets.all(24), + alignment: Alignment.center, + child: Text(emptyMessage, + style: GoogleFonts.inter(fontSize: 13, color: outline)), + ), + ), + ); + } + return RefreshIndicator( + onRefresh: _fetchTasks, + child: ListView.separated( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), + itemCount: list.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) => _buildTaskCard(list[index]), + ), + ); + } + + Widget _buildTaskCard(Map task) { + bool isJemput = task['task_type'] == 'jemput'; + String customerName = task['customer_name'] ?? '-'; + String address = task['address'] ?? '-'; + String phone = task['phone'] ?? ''; + String orderNumber = task['order_number'] ?? ''; + if (!orderNumber.startsWith('#') && orderNumber.isNotEmpty) { + orderNumber = '#$orderNumber'; + } + String lat = task['latitude']?.toString() ?? ''; + String lng = task['longitude']?.toString() ?? ''; + + final payMethod = (task['payment_method'] ?? "").toString().toUpperCase(); + final cashRec = num.tryParse(task['cash_received']?.toString() ?? "0") ?? 0; + final isUnpaid = payMethod == "BAYAR NANTI" && cashRec == 0; + + return Container( + decoration: BoxDecoration( + color: surfaceContainerLowest, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: outlineVariant.withOpacity(0.3)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Header (Order #, Name, Phone, Badges) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(orderNumber, + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.bold, + color: primary, + letterSpacing: -0.2)), + const SizedBox(height: 2), + Text(customerName, + style: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w600, + color: onSurface)), + if (phone.isNotEmpty) ...[ + const SizedBox(height: 2), + Row( + children: [ + Icon(Icons.call_rounded, + size: 14, color: outline), + const SizedBox(width: 6), + Text(phone, + style: GoogleFonts.inter( + fontSize: 11, color: outline)), + ], + ), + ], + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (!isJemput) _buildPaymentBadge(task), + if (isJemput) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(999), + border: + Border.all(color: primary.withOpacity(0.2)), + ), + child: Text( + "PICK-UP", + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.bold, + color: primary, + ), + ), + ), + if (!isJemput) ...[ + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: secondaryContainer.withOpacity(0.5), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + children: [ + Icon(Icons.local_shipping_rounded, + size: 14, color: onSecondaryContainer), + const SizedBox(width: 4), + Text("Delivery", + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.w500, + color: onSecondaryContainer)), + ], + ), + ), + ], + ], + ), + ], + ), + const SizedBox(height: 12), + + // Nota for Delivery + if (!isJemput) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: surfaceContainerLow.withOpacity(0.5), + borderRadius: BorderRadius.circular(12), + border: + Border.all(color: outlineVariant.withOpacity(0.1)), + ), + child: Column( + children: [ + // Services + if (task['service_name'] != null && + task['service_name'].toString().isNotEmpty) ...[ + ...task['service_name'] + .toString() + .split(RegExp(r' \+ |\n')) + .map((item) { + List parts = item.split('||'); + String namePart = parts[0]; + String pricePart = parts.length > 1 ? parts[1] : ""; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Icon(Icons.dry_cleaning_rounded, + size: 18, color: primary), + const SizedBox(width: 8), + Expanded( + child: Text(namePart, + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w500, + color: onSurface)), + ), + ], + ), + ), + if (pricePart.isNotEmpty) + Text("Rp ${formatHarga(pricePart)}", + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.bold, + color: primary)), + ], + ), + ); + }), + ], + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.only(top: 8), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: outlineVariant.withOpacity(0.1))), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("TOTAL TAGIHAN", + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.bold, + color: outline, + letterSpacing: 1.0)), + Text("Rp ${formatHarga(task['total_price'])}", + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.bold, + color: onSurface)), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 8), + ], + + // Address + if (isJemput) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: surfaceContainerLow.withOpacity(0.5), + borderRadius: BorderRadius.circular(12), + border: + Border.all(color: outlineVariant.withOpacity(0.1)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.location_on_rounded, + size: 18, color: primary), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("ALAMAT PICK UP", + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.bold, + color: outline, + letterSpacing: 0.5)), + const SizedBox(height: 2), + Text(address, + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w500, + color: onSurface)), + ], + ), + ), + ], + ), + ) + else + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.location_on_rounded, + size: 16, color: outline), + const SizedBox(width: 6), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("ALAMAT DELIVERY", + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.bold, + color: outline, + letterSpacing: 0.5)), + const SizedBox(height: 2), + Text(address, + style: GoogleFonts.inter( + fontSize: 11, + color: onSurface, + height: 1.2)), + ], + ), + ), + ], + ), + ), + + // Buttons Map & WA + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: InkWell( + onTap: () => _openGoogleMaps(address, lat, lng), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.map_rounded, + size: 18, color: onSurfaceVariant), + const SizedBox(width: 8), + Text("Rute", + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w500, + color: onSurfaceVariant)), + ], + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: InkWell( + onTap: (_sentMessages.contains(task['type_id']) || + _sendingWhatsAppTaskIds.contains(task['type_id'])) + ? null + : () => _sendWhatsApp(phone, customerName, + orderNumber, task['task_type'], task['type_id']), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: _sentMessages.contains(task['type_id']) + ? surfaceContainerHigh + : (_sendingWhatsAppTaskIds.contains(task['type_id']) + ? secondaryContainer.withOpacity(0.25) + : secondaryContainer.withOpacity(0.5)), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_sendingWhatsAppTaskIds.contains(task['type_id'])) ...[ + SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator( + color: onSecondaryContainer, + strokeWidth: 2, + ), + ), + const SizedBox(width: 8), + Text( + "Mengirim...", + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w500, + color: onSecondaryContainer, + ), + ), + ] else ...[ + Icon( + _sentMessages.contains(task['type_id']) + ? Icons.check_circle_rounded + : Icons.chat_rounded, + size: 18, + color: _sentMessages.contains(task['type_id']) + ? outline + : onSecondaryContainer, + ), + const SizedBox(width: 8), + Text( + _sentMessages.contains(task['type_id']) + ? "Terkirim" + : "Konfirmasi", + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w500, + color: _sentMessages.contains(task['type_id']) + ? outline + : onSecondaryContainer, + ), + ), + ], + ], + ), + ), + ), + ), + ], + ), + + if (isJemput) ...[ + const SizedBox(height: 12), + InkWell( + onTap: _submittingTaskIds.contains(task['type_id']) + ? null + : () => _updateTaskStatus(task), + borderRadius: BorderRadius.circular(12), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: _submittingTaskIds.contains(task['type_id']) + ? primary.withOpacity(0.6) + : primary, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 2, + offset: const Offset(0, 1)) + ], + ), + child: Center( + child: _submittingTaskIds.contains(task['type_id']) + ? const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Text("Finish Pick-up", + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + color: onPrimary)), + ), + ), + ), + ], + ], + ), + ), + if (!isJemput) + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: surfaceContainerLowest, + border: Border( + top: BorderSide(color: outlineVariant.withOpacity(0.1))), + borderRadius: + const BorderRadius.vertical(bottom: Radius.circular(16)), + ), + child: InkWell( + onTap: _submittingTaskIds.contains(task['type_id']) + ? null + : () { + if (isUnpaid) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => HomePaymentPage( + order: task, + userData: widget.userData, + ), + ), + ).then((_) => _fetchTasks()); + } else { + _updateTaskStatus(task); + } + }, + borderRadius: BorderRadius.circular(12), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + color: _submittingTaskIds.contains(task['type_id']) + ? primary.withOpacity(0.6) + : primary, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 2, + offset: const Offset(0, 1)) + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: _submittingTaskIds.contains(task['type_id']) + ? [ + const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + ] + : [ + Text("Finish Delivery", + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: onPrimary)), + const SizedBox(width: 8), + Icon(Icons.check_circle_rounded, + size: 18, color: onPrimary), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/laundry_mobile/lib/driver/profile.dart b/laundry_mobile/lib/driver/profile.dart new file mode 100644 index 0000000..8f0c98e --- /dev/null +++ b/laundry_mobile/lib/driver/profile.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../login.dart'; + +class ProfilePage extends StatelessWidget { + final Map userData; + final bool isTab; + const ProfilePage({super.key, required this.userData, this.isTab = true}); + + // --- Tailwind DESIGN TOKENS --- + final Color primary = const Color(0xFF0058BE); + final Color onPrimary = const Color(0xFFFFFFFF); + final Color primaryContainer = const Color(0xFF2170E4); + final Color background = const Color(0xFFF7F9FB); + final Color surface = const Color(0xFFF7F9FB); + final Color surfaceContainerLowest = const Color(0xFFFFFFFF); + final Color surfaceContainerLow = const Color(0xFFF0F4F8); + final Color onSurface = const Color(0xFF191C1E); + final Color onSurfaceVariant = const Color(0xFF424754); + final Color outlineVariant = const Color(0xFFC2C6D6); + final Color outline = const Color(0xFF727785); + final Color error = const Color(0xFFBA1A1A); + + @override + Widget build(BuildContext context) { + String displayName = userData['user']?['name'] ?? 'Driver'; + String email = userData['user']?['email'] ?? "Tidak ada email"; + + Widget content = SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildProfileHero(displayName, email), + const SizedBox(height: 24), + _buildLogoutButton(context), + const SizedBox(height: 32), + _buildVersionFooter(), + const SizedBox(height: 24), // padding for bottom nav + ], + ), + ), + ); + + if (isTab) return content; + + return Scaffold( + backgroundColor: surface, + body: content, + ); + } + + Widget _buildProfileHero(String name, String email) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [ + Color(0xFF2170E4), + Color(0xFF004395) + ], // Matches HTML linear-gradient + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2)) + ], + ), + child: Stack( + children: [ + // Decorative circle + Positioned( + right: -16, + top: -16, + child: Container( + width: 96, + height: 96, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withOpacity(0.05), + ), + ), + ), + Row( + children: [ + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withOpacity(0.3), width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), blurRadius: 2) + ], + ), + child: CircleAvatar( + backgroundColor: Colors.white, + child: Icon(Icons.person, color: primary, size: 40), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white, + letterSpacing: -0.5), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + email, + style: GoogleFonts.inter( + fontSize: 13, color: Colors.white.withOpacity(0.9)), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ) + ], + ) + ], + ), + ); + } + + Widget _buildLogoutButton(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('userData'); + await prefs.remove('sent_wa_messages'); + await prefs.remove('sent_wa_messages_v2'); + if (!context.mounted) return; + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const LoginPage()), + (route) => false); + }, + borderRadius: BorderRadius.circular(12), + highlightColor: error.withOpacity(0.1), + splashColor: error.withOpacity(0.05), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + border: Border.all(color: outlineVariant.withOpacity(0.3)), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.logout, color: error, size: 20), + const SizedBox(width: 8), + Text( + "Keluar", + style: GoogleFonts.inter( + fontSize: 14, fontWeight: FontWeight.w500, color: error), + ), + ], + ), + ), + ), + ); + } + + Widget _buildVersionFooter() { + return Center( + child: Text( + "", + textAlign: TextAlign.center, + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w500, + letterSpacing: 0.5, + color: onSurfaceVariant.withOpacity(0.4), + ), + ), + ); + } +} diff --git a/laundry_mobile/lib/login.dart b/laundry_mobile/lib/login.dart new file mode 100644 index 0000000..83c5b2f --- /dev/null +++ b/laundry_mobile/lib/login.dart @@ -0,0 +1,445 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'dart:async'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import 'staf/beranda.dart'; +import 'driver/beranda.dart' as driver_beranda; +import 'config.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State with TickerProviderStateMixin { + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + bool _isLoading = false; + bool _obscureText = true; + + // --- UNIFIED DESIGN TOKENS (Matched with other pages) --- + final Color bgLight = const Color(0xFFF7F9FB); + final Color surfaceColor = const Color(0xFFFFFFFF); + final Color primaryColor = const Color(0xFF0058BE); + final Color primaryDark = const Color(0xFF004596); + final Color textDark = const Color(0xFF191C1E); + final Color textGray = const Color(0xFF424754); + final Color borderLight = const Color(0xFFC2C6D6); + + late AnimationController _headerController; + late Animation _headerAnimation; + + @override + void initState() { + super.initState(); + _headerController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + ); + _headerAnimation = + CurvedAnimation(parent: _headerController, curve: Curves.easeOutBack); + _headerController.forward(); + } + + @override + void dispose() { + _headerController.dispose(); + super.dispose(); + } + + Future _login() async { + if (_emailController.text.isEmpty || _passwordController.text.isEmpty) { + _showSnackBar("Harap isi semua bidang", Colors.orange); + return; + } + + setState(() => _isLoading = true); + try { + final response = await http.post( + Uri.parse("${AppConfig.baseUrl}/login"), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: jsonEncode({ + 'email': _emailController.text.trim(), + 'password': _passwordController.text, + }), + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('userData', response.body); + + if (!mounted) return; + + String userRole = data['user']?['role'] ?? 'staf'; + + if (userRole == 'driver') { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => + driver_beranda.BerandaPage(userData: data))); + } else { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => BerandaPage(userData: data))); + } + } else { + _showSnackBar("Email atau password salah", Colors.redAccent); + } + } catch (e) { + _showSnackBar("Terjadi kesalahan koneksi", Colors.redAccent); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + void _showSnackBar(String msg, Color bg) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Icon( + bg == Colors.green + ? Icons.check_circle_outline + : Icons.error_outline_rounded, + color: Colors.white, + size: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + bg == Colors.green ? "Berhasil" : "Peringatan", + style: GoogleFonts.inter( + fontWeight: FontWeight.w800, + fontSize: 14, + color: Colors.white), + ), + Text( + msg, + style: GoogleFonts.inter( + fontSize: 12, + color: Colors.white.withOpacity(0.9), + fontWeight: FontWeight.w500), + ), + ], + ), + ), + ], + ), + ), + behavior: SnackBarBehavior.floating, + backgroundColor: bg.withOpacity(0.95), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: Colors.white.withOpacity(0.2), width: 1), + ), + duration: const Duration(seconds: 3), + )); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: bgLight, + body: Stack( + children: [ + _buildBackgroundGradient(), + SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + const SizedBox(height: 60), + _buildHeaderLogo(), + const SizedBox(height: 48), + _buildLoginCard(), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildBackgroundGradient() { + return Container( + height: MediaQuery.of(context).size.height * 0.4, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [primaryColor, primaryDark]), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40)), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10)) + ], + ), + ); + } + + Widget _buildHeaderLogo() { + return ScaleTransition( + scale: _headerAnimation, + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + shape: BoxShape.circle), + child: GestureDetector( + onDoubleTap: () => + _showIPDialog(context), // Rahasia: Tap 2x di logo + child: SizedBox( + width: 48, + height: 48, + child: CustomPaint(painter: MLogoPainter())), + )), + const SizedBox(height: 16), + // NAMA APLIKASI DIUBAH DI SINI + Text("My Laundry", + style: GoogleFonts.inter( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.w800, + letterSpacing: -1)), + Text("Laundry Management System", + style: GoogleFonts.inter( + color: Colors.white.withOpacity(0.7), + fontSize: 13, + fontWeight: FontWeight.w500)), + ], + ), + ); + } + + Widget _buildLoginCard() { + return Container( + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: surfaceColor, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: borderLight, width: 1.0), // Border tipis sesuai standar + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 20, + offset: const Offset(0, 8)) + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Selamat Datang", + style: GoogleFonts.inter( + color: textDark, + fontSize: 22, + fontWeight: FontWeight.w800, + letterSpacing: -0.5)), + const SizedBox(height: 4), + Text("Masuk untuk mulai mengelola outlet", + style: GoogleFonts.inter( + color: textGray, fontSize: 13, fontWeight: FontWeight.w500)), + const SizedBox(height: 32), + _buildField( + "Email Address", Icons.alternate_email_rounded, _emailController), + const SizedBox(height: 16), + _buildField( + "Password", Icons.lock_outline_rounded, _passwordController, + isPass: true), + const SizedBox(height: 32), + _buildSubmitButton(), + ], + ), + ); + } + + Widget _buildField(String label, IconData icon, TextEditingController ctrl, + {bool isPass = false}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: GoogleFonts.inter( + color: textDark, fontWeight: FontWeight.w700, fontSize: 12)), + const SizedBox(height: 8), + TextField( + controller: ctrl, + obscureText: isPass && _obscureText, + style: GoogleFonts.inter( + color: textDark, fontWeight: FontWeight.w600, fontSize: 14), + decoration: InputDecoration( + prefixIcon: Icon(icon, color: textGray, size: 20), + suffixIcon: isPass + ? IconButton( + icon: Icon( + _obscureText + ? Icons.visibility_off_rounded + : Icons.visibility_rounded, + color: textGray, + size: 18), + onPressed: () => + setState(() => _obscureText = !_obscureText)) + : null, + filled: true, + fillColor: bgLight, + hintText: "Masukkan $label", + hintStyle: GoogleFonts.inter( + color: textGray.withOpacity(0.5), + fontSize: 13), // Opacity disesuaikan sedikit agar lebih terbaca + contentPadding: const EdgeInsets.symmetric(vertical: 16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: borderLight, + width: 1.0)), // Diubah dari none ke borderLight tipis + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: borderLight, width: 1.0)), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: primaryColor, + width: 1.0)), // Menyesuaikan style input + ), + ), + ], + ); + } + + Widget _buildSubmitButton() { + return SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + elevation: 0, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: _isLoading ? null : _login, + child: _isLoading + ? const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + color: Colors.white, strokeWidth: 2.5)) + : Text("MASUK SEKARANG", + style: GoogleFonts.inter( + fontWeight: FontWeight.w800, + color: Colors.white, + letterSpacing: 0.5)), + ), + ); + } + + void _showIPDialog(BuildContext context) { + final controller = TextEditingController(text: AppConfig.rawIp); + showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text("Pengaturan IP Server", + style: GoogleFonts.inter(fontWeight: FontWeight.w800)), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Masukkan IP laptop Anda untuk menghubungkan aplikasi ke server.", + style: GoogleFonts.inter(fontSize: 13, color: textGray)), + const SizedBox(height: 20), + TextField( + controller: controller, + decoration: InputDecoration( + hintText: "Contoh: 192.168.1.XX", + filled: true, + fillColor: bgLight, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text("BATAL", + style: GoogleFonts.inter( + fontWeight: FontWeight.bold, color: textGray)), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + onPressed: () async { + await AppConfig.setIp(controller.text.trim()); + if (!context.mounted) return; + Navigator.pop(context); + _showSnackBar("IP Server berhasil diperbarui!", Colors.green); + }, + child: Text("SIMPAN", + style: GoogleFonts.inter( + fontWeight: FontWeight.bold, color: Colors.white)), + ), + ], + ), + ); + } +} + +class MLogoPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paintLine = Paint() + ..color = Colors.white + ..strokeWidth = size.width * 0.1 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; + final paintDot = Paint() + ..color = const Color(0xFF10B981) + ..style = PaintingStyle.fill; + double scale = size.width / 512.0; + Path path = Path() + ..moveTo(120 * scale, 360 * scale) + ..lineTo(120 * scale, 220 * scale) + ..cubicTo(120 * scale, 180 * scale, 150 * scale, 160 * scale, 180 * scale, + 190 * scale) + ..lineTo(256 * scale, 266 * scale) + ..lineTo(332 * scale, 190 * scale) + ..cubicTo(362 * scale, 160 * scale, 392 * scale, 180 * scale, 392 * scale, + 220 * scale) + ..lineTo(392 * scale, 360 * scale); + canvas.drawPath(path, paintLine); + canvas.drawCircle(Offset(256 * scale, 110 * scale), 32 * scale, paintDot); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/laundry_mobile/lib/main.dart b/laundry_mobile/lib/main.dart new file mode 100644 index 0000000..614051a --- /dev/null +++ b/laundry_mobile/lib/main.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; +import 'package:firebase_database/firebase_database.dart'; + +import 'login.dart'; +import 'staf/beranda.dart'; +import 'config.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + try { + await Firebase.initializeApp(); + FirebaseDatabase.instance + .ref('laundry/screen_status') + .onDisconnect() + .set("RESTART"); + FirebaseDatabase.instance + .ref('laundry/status_hp') + .onDisconnect() + .set("-|-|-|0.000|Rp0"); + } catch (e) { + debugPrint("Firebase failed to initialize: $e"); + } + await AppConfig.loadConfig(); + + // Cek apakah ada data sesi login yang tersimpan + final prefs = await SharedPreferences.getInstance(); + final String? userDataString = prefs.getString('userData'); + + Widget initialPage = const LoginPage(); + + if (userDataString != null) { + try { + final userData = jsonDecode(userDataString); + // Jika berhasil decode, langsung masuk ke Beranda + initialPage = BerandaPage(userData: userData); + } catch (e) { + // Jika gagal decode (misal corrupted), biarkan tetap di LoginPage + } + } + + runApp(MyApp(initialPage: initialPage)); +} + +class MyApp extends StatefulWidget { + final Widget initialPage; + + const MyApp({super.key, required this.initialPage}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused || + state == AppLifecycleState.detached) { + try { + FirebaseDatabase.instance.ref('laundry/screen_status').set("RESTART"); + FirebaseDatabase.instance + .ref('laundry/status_hp') + .set("-|-|-|0.000|Rp0"); + } catch (e) { + debugPrint("Failed to send RESTART on exit: $e"); + } + } + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Management Laundry', + theme: ThemeData( + fontFamily: 'Manrope', + useMaterial3: true, + scaffoldBackgroundColor: const Color(0xFFF2F7FB), + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: CupertinoPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + }, + ), + ), + home: widget.initialPage, + ); + } +} diff --git a/laundry_mobile/lib/staf/beranda.dart b/laundry_mobile/lib/staf/beranda.dart new file mode 100644 index 0000000..4516135 --- /dev/null +++ b/laundry_mobile/lib/staf/beranda.dart @@ -0,0 +1,1033 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '../config.dart'; +import 'dart:async'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:firebase_database/firebase_database.dart'; + +import 'home/order_list.dart'; +import 'home/payments.dart'; +import 'laundry.dart'; +import 'profile.dart'; +import 'request_pickup.dart'; + +class BerandaPage extends StatefulWidget { + final Map userData; + const BerandaPage({super.key, required this.userData}); + + @override + State createState() => _BerandaPageState(); +} + +class _BerandaPageState extends State { + int _selectedIndex = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF7F9FB), // bg-surface + body: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: _selectedIndex == 0 + ? HomeTab(key: const ValueKey("home"), userData: widget.userData) + : _selectedIndex == 1 + ? LaundryPage( + key: const ValueKey("laundry"), + userData: widget.userData, + isTab: true) + : ProfilePage( + key: const ValueKey("profile"), + userData: widget.userData, + isTab: true), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition(opacity: animation, child: child); + }, + ), + bottomNavigationBar: _buildPersistentBottomNav(), + ); + } + + Widget _buildPersistentBottomNav() { + return Container( + height: 64 + MediaQuery.of(context).padding.bottom, + decoration: BoxDecoration( + color: const Color(0xFFFFFFFF).withOpacity(0.95), + border: + const Border(top: BorderSide(color: Color(0xFFE6E8EA), width: 1.0)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, -1)) + ], + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _navItem(Icons.home, "Beranda", _selectedIndex == 0, () { + setState(() => _selectedIndex = 0); + }), + _navItem(Icons.add_circle, "Input", _selectedIndex == 1, + () async { + setState(() => _selectedIndex = 1); + try { + await FirebaseDatabase.instance + .ref('laundry/screen_status') + .set("RESTART"); + // Otomatis kembalikan ke ON dari sisi HP agar tidak nyangkut + await Future.delayed(const Duration(milliseconds: 500)); + await FirebaseDatabase.instance + .ref('laundry/screen_status') + .set("ON"); + // ignore: empty_catches + } catch (e) {} + }), + _navItem(Icons.person, "Profil", _selectedIndex == 2, () { + setState(() => _selectedIndex = 2); + }), + ], + ), + ), + ), + ); + } + + Widget _navItem( + IconData icon, String label, bool isActive, VoidCallback onTap) { + return InkWell( + onTap: onTap, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + child: Container( + width: 64, + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 24, + color: isActive + ? const Color(0xFF0058BE) + : const Color(0xFF424754).withOpacity(0.6), + ), + const SizedBox(height: 2), + Text( + label, + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: isActive ? FontWeight.w700 : FontWeight.w500, + color: isActive + ? const Color(0xFF0058BE) + : const Color(0xFF424754).withOpacity(0.6), + ), + ), + ], + ), + ), + ); + } +} + +class HomeTab extends StatefulWidget { + final Map userData; + const HomeTab({super.key, required this.userData}); + + @override + State createState() => _HomeTabState(); +} + +class _HomeTabState extends State { + // Tailwind Colors + final Color primary = const Color(0xFF0058BE); + final Color surface = const Color(0xFFF7F9FB); + final Color surfaceContainerHigh = const Color(0xFFE6E8EA); + final Color onSurface = const Color(0xFF191C1E); + final Color onSurfaceVariant = const Color(0xFF424754); + final Color primaryFixed = const Color(0xFFD8E2FF); + final Color surfaceContainerLowest = const Color(0xFFFFFFFF); + final Color surfaceContainerLow = const Color(0xFFF2F4F6); + final Color secondaryContainer = const Color(0xFFC3ECD7); + final Color onSecondaryContainer = const Color(0xFF294E3F); + final Color outline = const Color(0xFF727785); + + int countAntrian = 0; + int countProses = 0; + int countSelesai = 0; + List aktivitasTerbaru = []; + List pesananTiba = []; + bool isLoading = true; + Timer? _refreshTimer; + + String _selectedTab = 'antrian'; + + static Map? _cachedStats; + static Map? _cachedActs; + static Map? _cachedTiba; + + @override + void initState() { + super.initState(); + if (_cachedStats != null && _cachedActs != null) { + _loadFromCache(); + } + fetchAllData(showLoading: _cachedStats == null); + _startRefreshTimer(); + } + + void _loadFromCache() { + countAntrian = _cachedStats!['data']['antrian'] ?? 0; + countProses = _cachedStats!['data']['proses'] ?? 0; + countSelesai = _cachedStats!['data']['selesai'] ?? 0; + aktivitasTerbaru = _cachedActs!['data'] ?? []; + pesananTiba = _cachedTiba!['data'] ?? []; + isLoading = false; + } + + void _startRefreshTimer() { + _refreshTimer?.cancel(); + _refreshTimer = Timer.periodic(const Duration(seconds: 3), (timer) { + if (mounted) fetchAllData(showLoading: false); + }); + } + + @override + void dispose() { + _refreshTimer?.cancel(); + super.dispose(); + } + + Future fetchAllData({bool showLoading = true}) async { + if (showLoading) setState(() => isLoading = true); + String baseUrl = AppConfig.baseUrl; + + try { + final String? token = widget.userData['access_token']; + final responses = await Future.wait([ + http.get(Uri.parse('$baseUrl/dashboard/stats'), headers: { + 'Authorization': 'Bearer $token', + 'Accept': 'application/json', + }), + http.get(Uri.parse('$baseUrl/dashboard/activities'), headers: { + 'Authorization': 'Bearer $token', + 'Accept': 'application/json', + }), + http.get(Uri.parse('$baseUrl/orders/status/TIBA%20DI%20TOKO'), + headers: { + 'Authorization': 'Bearer $token', + 'Accept': 'application/json', + }), + http.get(Uri.parse('$baseUrl/shop/settings'), headers: { + 'Authorization': 'Bearer $token', + 'Accept': 'application/json', + }), + ]); + + if (responses[0].statusCode == 200 && responses[1].statusCode == 200) { + final dataStats = json.decode(responses[0].body); + final dataActs = json.decode(responses[1].body); + final dataTiba = responses.length > 2 && responses[2].statusCode == 200 + ? json.decode(responses[2].body) + : {'data': []}; + + if (responses.length > 3 && responses[3].statusCode == 200) { + final dataSettings = json.decode(responses[3].body); + if (dataSettings['data'] != null) { + widget.userData['shop'] = dataSettings['data']; + } + } + + _cachedStats = dataStats; + _cachedActs = dataActs; + _cachedTiba = dataTiba; + + setState(() { + countAntrian = dataStats['data']['antrian'] ?? 0; + countProses = dataStats['data']['proses'] ?? 0; + countSelesai = dataStats['data']['selesai'] ?? 0; + + aktivitasTerbaru = dataActs['data'] ?? []; + + pesananTiba = dataTiba['data'] ?? []; + isLoading = false; + }); + } else { + if (mounted && _cachedStats == null) setState(() => isLoading = false); + } + } catch (e) { + if (mounted && _cachedStats == null) setState(() => isLoading = false); + } + } + + String formatHarga(dynamic value) { + if (value == null) return "0"; + String val = value.toString(); + RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'); + return val.replaceAllMapped(reg, (Match m) => '${m[1]}.'); + } + + String formatWeightInString(String input) { + return input.replaceAllMapped(RegExp(r'\((\d+\.?\d*)\s*([a-zA-Z]+)\)'), + (match) { + double val = double.tryParse(match.group(1) ?? "0") ?? 0; + String unit = match.group(2) ?? "Kg"; + if (unit.toUpperCase() == 'PCS') return "(${val.toInt()} $unit)"; + + // Format the display string exactly with 3 decimal places (e.g. 2.500 instead of 2.5) + String valStr = val.toStringAsFixed(3); + + return "($valStr $unit)"; + }); + } + + List get _filteredActivities { + List filtered = aktivitasTerbaru.where((item) { + String status = item['status']?.toString().toUpperCase() ?? ""; + if (_selectedTab == 'antrian' && + (status == 'ANTRIAN' || status == 'MENUNGGU JEMPUT')) return true; + if (_selectedTab == 'proses' && + (status == 'PROSES' || + status == 'JEMPUTAN TIBA' || + status == 'MENCUCI' || + status == 'MENYETRIKA')) return true; + if (_selectedTab == 'selesai' && status == 'SELESAI') return true; + return false; + }).toList(); + + filtered.sort((a, b) { + int priority(String type) { + String t = type.toLowerCase(); + if (t.contains('kilat')) return 1; + if (t.contains('ekspres') || t.contains('express')) return 2; + return 3; + } + + int pA = priority((a['estimation_type'] ?? '').toString()); + int pB = priority((b['estimation_type'] ?? '').toString()); + if (pA != pB) return pA.compareTo(pB); + + final fallbackTime = DateTime(2100, 1, 1); + DateTime parseTime(dynamic timeStr) { + if (timeStr == null || timeStr.toString().isEmpty) { + return fallbackTime; + } + return DateTime.tryParse(timeStr.toString()) ?? fallbackTime; + } + + DateTime timeA = parseTime(a['estimation_time']); + DateTime timeB = parseTime(b['estimation_time']); + int comp = timeA.compareTo(timeB); + if (comp != 0) return comp; + return (a['id'] as int) + .compareTo(b['id'] as int); // tertiary sort oldest first + }); + + return filtered; + } + + @override + Widget build(BuildContext context) { + return isLoading + ? Center(child: CircularProgressIndicator(color: primary)) + : Column( + children: [ + _buildAppBar(), + Expanded( + child: RefreshIndicator( + onRefresh: () => fetchAllData(), + color: primary, + backgroundColor: Colors.white, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics()), + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 80), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // HERO BANNER + GestureDetector( + onTap: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + RequestPickupPage( + userData: widget.userData))); + if (result == true) fetchAllData(); + }, + child: Container( + width: double.infinity, + height: 160, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: + Border.all(color: surfaceContainerHigh), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 4, + offset: const Offset(0, 2)) + ], + image: const DecorationImage( + image: AssetImage('assets/beranda.jpg'), + fit: BoxFit.cover, + ), + ), + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(12), + gradient: LinearGradient( + colors: [ + primary.withOpacity(0.8), + primary.withOpacity(0.4), + Colors.transparent + ], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 24), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + "Halo, ${(widget.userData['user']?['name'] ?? 'Staf').toString().split(' ')[0]}", + style: GoogleFonts.inter( + fontSize: 22, + fontWeight: FontWeight.w700, + color: Colors.white, + height: 1.1, + ), + ), + const SizedBox(height: 4), + Text( + "Siap mengelola pesanan hari ini?", + style: GoogleFonts.inter( + fontSize: 12, + color: Colors.white + .withOpacity(0.9), + ), + ), + const SizedBox(height: 12), + Container( + padding: + const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular(999), + boxShadow: [ + BoxShadow( + color: Colors.black + .withOpacity(0.05), + blurRadius: 4, + offset: + const Offset(0, 2)) + ], + ), + child: Text( + "Buat Jadwal Pickup", + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w700, + color: primary, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(height: 24), + + // ORDER TABS + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text("Status Pesanan", + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w600, + color: onSurface)), + GestureDetector( + onTap: () { + String initialFilter = "Semua"; + if (_selectedTab == 'antrian') { + initialFilter = "Antrian"; + } + if (_selectedTab == 'proses') { + initialFilter = "Proses"; + } + if (_selectedTab == 'selesai') { + initialFilter = "Selesai"; + } + Navigator.push( + context, + MaterialPageRoute( + builder: (c) => OrderListPage( + initialFilter: initialFilter, + userData: widget.userData))); + }, + child: Text("Lihat Semua", + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + color: primary)), + ), + ], + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: surfaceContainerLow, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + _buildTabItem( + 'antrian', 'Antrian ($countAntrian)'), + _buildTabItem( + 'proses', 'Proses ($countProses)'), + _buildTabItem( + 'selesai', 'Selesai ($countSelesai)'), + ], + ), + ), + const SizedBox(height: 12), + + // TAB CONTENT (Filtered Orders) + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: _buildFilteredList( + key: ValueKey(_selectedTab)), + ), + + const SizedBox(height: 24), + + // PESANAN TIBA DI TOKO + if (pesananTiba.isNotEmpty) ...[ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text("Tiba di Toko (Belum Diinput)", + style: GoogleFonts.inter( + fontSize: 15, + fontWeight: FontWeight.w700, + color: onSurface)), + if (pesananTiba.length > 3) + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (c) => OrderListPage( + initialFilter: "Proses", + userData: + widget.userData))); + }, + child: Text("Lihat Semua", + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + color: primary)), + ), + ], + ), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: primary.withOpacity(0.2)), + ), + child: Column( + children: pesananTiba + .take(3) + .toList() + .asMap() + .entries + .map((entry) { + return _buildTibaRow( + entry.value, + entry.key == + (pesananTiba.take(3).length - 1)); + }).toList(), + ), + ), + const SizedBox(height: 24), + ], + + // LAST ACTIVITY + Text("Aktivitas Terakhir", + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w600, + color: onSurface)), + const SizedBox(height: 12), + _buildRecentActivityList(), + ], + ), + ), + ), + ], + ), + ), // End of RefreshIndicator + ), // End of Expanded + ], // End of Column children + ); // End of Column + } + + Widget _buildTabItem(String tabId, String label) { + bool isActive = _selectedTab == tabId; + return Expanded( + child: GestureDetector( + onTap: () => setState(() => _selectedTab = tabId), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 6), + decoration: BoxDecoration( + color: isActive ? surfaceContainerLowest : Colors.transparent, + borderRadius: BorderRadius.circular(8), + boxShadow: isActive + ? [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 2, + offset: const Offset(0, 1)) + ] + : null, + ), + alignment: Alignment.center, + child: Text( + label, + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w500, + color: isActive ? primary : onSurfaceVariant, + ), + ), + ), + ), + ); + } + + Widget _buildFilteredList({Key? key}) { + List items = _filteredActivities; + if (items.isEmpty) { + return Container( + key: key, + width: double.infinity, + padding: const EdgeInsets.all(24), + alignment: Alignment.center, + child: Text("Belum ada pesanan", + style: GoogleFonts.inter(fontSize: 13, color: outline)), + ); + } + + return Column( + key: key, + children: + items.take(2).map((item) => _buildOrderCard(item)).toList(), + ); + } + + Widget _buildRecentActivityList() { + if (aktivitasTerbaru.isEmpty) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + alignment: Alignment.center, + child: Text("Belum ada aktivitas", + style: GoogleFonts.inter(fontSize: 13, color: outline)), + ); + } + + return Column( + children: aktivitasTerbaru + .take(5) + .map((item) => _buildOrderCard(item, isClickable: false)) + .toList(), + ); + } + + Widget _buildOrderCard(dynamic item, {bool isClickable = true}) { + String title = item['title']?.toString() ?? "Pesanan"; + String rawSubtitle = item['subtitle']?.toString() ?? "-"; + String subtitle = formatWeightInString(rawSubtitle.split('||').first); + String status = item['status']?.toString().toUpperCase() ?? "PROSES"; + String orderId = "#${item['order_number'] ?? item['id'] ?? 'ACT'}"; + + String price = ""; + if (item['total'] != null) { + price = "Rp ${formatHarga(item['total'])}"; + } + + if (status == 'MENUNGGU JEMPUT') status = 'PICKUP'; + if (status == 'JEMPUTAN TIBA') status = 'DI TOKO'; + + IconData icon = Icons.local_laundry_service; + if (status == 'ANTRIAN' || status == 'PICKUP') icon = Icons.dry_cleaning; + if (status == 'SELESAI') icon = Icons.checkroom; + if (status == 'DI DRIVER') icon = Icons.delivery_dining; + + return InkWell( + onTap: isClickable + ? () { + if (item['status']?.toString().toUpperCase() == 'ANTRIAN' || + item['status']?.toString().toUpperCase() == 'PROSES' || + item['status']?.toString().toUpperCase() == 'SELESAI') { + _showUpdateStatusDialog(item); + } + } + : null, + child: Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: surfaceContainerLowest, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: surfaceContainerHigh), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: primary.withOpacity(0.05), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: primary.withOpacity(0.1)), + ), + alignment: Alignment.center, + child: Icon(icon, color: primary, size: 22), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(orderId, + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w500, + color: onSurfaceVariant.withOpacity(0.7), + letterSpacing: 0.5)), + Text(title, + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: onSurface, + height: 1.2), + maxLines: 1, + overflow: TextOverflow.ellipsis), + if (subtitle.isNotEmpty && subtitle != "-") ...[ + const SizedBox(height: 2), + Text(subtitle, + style: + GoogleFonts.inter(fontSize: 11, color: outline), + maxLines: 1, + overflow: TextOverflow.ellipsis), + ] + ], + ), + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: secondaryContainer.withOpacity(0.5), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + status, + style: GoogleFonts.inter( + fontSize: 9, + fontWeight: FontWeight.w700, + color: onSecondaryContainer, + letterSpacing: 0.5), + ), + ), + if (price.isNotEmpty && price != "Rp 0") ...[ + const SizedBox(height: 4), + Text(price, + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w700, + color: primary)), + ] + ], + ), + ], + ), + )); + } + + void _showUpdateStatusDialog(dynamic item) { + String currentStatus = item['status']?.toString().toUpperCase() ?? ""; + String orderIdStr = + item['order_number']?.toString() ?? item['id']?.toString() ?? ""; + int id = int.tryParse(item['id']?.toString() ?? "0") ?? 0; + if (id == 0) return; + + String promptText = ""; + bool isUnpaid = false; + + if (currentStatus == 'ANTRIAN') { + promptText = + "Mulai proses cucian (Mencuci/Menyetrika) untuk pesanan #$orderIdStr?"; + } else if (currentStatus == 'PROSES') { + promptText = "Tandai pesanan #$orderIdStr sebagai SELESAI?"; + } else if (currentStatus == 'SELESAI') { + String deliveryType = + item['delivery_type']?.toString().toLowerCase() ?? 'none'; + if (deliveryType == 'delivery' || deliveryType == 'both') { + promptText = + "Tugaskan pesanan #$orderIdStr ke Driver untuk pengiriman (DELIVERY)?"; + } else { + String paymentStatus = + item['payment_status']?.toString().toUpperCase() ?? "UNPAID"; + if (paymentStatus != 'PAID') { + isUnpaid = true; + promptText = + "Pesanan #$orderIdStr belum lunas. Lanjutkan ke halaman pelunasan?"; + } else { + promptText = + "Selesaikan pesanan #$orderIdStr dan ubah menjadi DIAMBIL?"; + } + } + } else { + return; + } + + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text("Lanjutkan Pesanan?", + style: GoogleFonts.inter(fontWeight: FontWeight.w700)), + content: Text(promptText, style: GoogleFonts.inter(fontSize: 14)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text("Batal", + style: GoogleFonts.inter(color: Colors.grey)), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: primary, foregroundColor: Colors.white), + onPressed: () { + Navigator.pop(ctx); + if (isUnpaid) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => HomePaymentPage( + order: item, + userData: widget.userData, + ), + ), + ).then((_) => fetchAllData()); + } else { + _updateOrderStatus(id); + } + }, + child: Text("Ya, Lanjutkan", + style: GoogleFonts.inter(fontWeight: FontWeight.w600)), + ), + ], + )); + } + + Future _updateOrderStatus(int id) async { + setState(() => isLoading = true); + try { + final String? token = widget.userData['access_token']; + final response = await http.put( + Uri.parse('${AppConfig.baseUrl}/orders/$id/update-status'), + headers: { + 'Authorization': 'Bearer $token', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + ); + if (response.statusCode == 200) { + fetchAllData(); + } else { + setState(() => isLoading = false); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Gagal memperbarui status"))); + } + } + } catch (e) { + if (mounted) setState(() => isLoading = false); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Terjadi kesalahan jaringan"))); + } + } + } + + Widget _buildTibaRow(dynamic order, bool isLast) { + String orderId = "#${order['order_number'] ?? order['id'] ?? 'ACT'}"; + String name = order['customer_name'] ?? "Pelanggan"; + + return InkWell( + onTap: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (c) => Scaffold( + appBar: AppBar( + title: Text("Input Layanan Pickup", + style: GoogleFonts.inter( + fontWeight: FontWeight.w700, fontSize: 16)), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded, + size: 20), + onPressed: () => Navigator.pop(c)), + ), + body: LaundryPage( + userData: widget.userData, + isTab: false, + pickupData: order)))); + if (result == true) fetchAllData(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: isLast + ? null + : Border( + bottom: + BorderSide(color: surfaceContainerHigh.withOpacity(0.5))), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(8)), + child: Icon(Icons.inventory_2_rounded, color: primary, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + fontSize: 14, + color: onSurface)), + const SizedBox(height: 2), + Text(orderId, + style: GoogleFonts.inter(fontSize: 11, color: outline)), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: primary, borderRadius: BorderRadius.circular(6)), + child: Text("Input", + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w700, + color: Colors.white)), + ) + ], + ), + ), + ); + } + + Widget _buildAppBar() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: surface.withOpacity(0.9), + border: Border( + bottom: BorderSide( + color: const Color(0xFFE6E8EA) + .withOpacity(0.5), // surfaceContainerHigh + width: 1.0, + ), + ), + ), + child: SafeArea( + bottom: false, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 32, + height: 32, + decoration: const BoxDecoration( + color: Color(0xFFD8E2FF), // primaryFixed + shape: BoxShape.circle, + ), + clipBehavior: Clip.hardEdge, + child: Image.asset( + 'assets/app_icon.png', + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Icon( + Icons.local_laundry_service_rounded, + color: primary, + size: 20), + ), + ), + const SizedBox(width: 8), + Text("My Laundry", + style: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w600, + color: primary, + )), + ], + ), + const SizedBox( + width: + 48), // Padding equivalent to empty action area to align center if needed, or just leave it empty. + ], + ), + ), + ); + } +} diff --git a/laundry_mobile/lib/staf/home/order_list.dart b/laundry_mobile/lib/staf/home/order_list.dart new file mode 100644 index 0000000..f1164ca --- /dev/null +++ b/laundry_mobile/lib/staf/home/order_list.dart @@ -0,0 +1,846 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '../../config.dart'; +import 'dart:async'; + +import 'package:google_fonts/google_fonts.dart'; +import 'payments.dart'; + +class OrderListPage extends StatefulWidget { + final String initialFilter; + final Map userData; + + const OrderListPage({ + super.key, + required this.initialFilter, + required this.userData, + }); + + @override + State createState() => _OrderListPageState(); +} + +class _OrderListPageState extends State { + late String _currentFilter; + + // --- UNIFIED DESIGN TOKENS --- + final Color bgBackground = const Color(0xFFf7f9fb); + final Color surfaceLowest = const Color(0xFFffffff); + final Color surfaceLow = const Color(0xFFf2f4f6); + final Color outlineVariant = const Color(0xFFc2c6d6); + final Color outlineColor = const Color(0xFF727785); + final Color primaryColor = const Color(0xFF0058be); + final Color primaryContainer = const Color(0xFF2170e4); + final Color errorColor = const Color(0xFFba1a1a); + final Color secondaryContainer = const Color(0xFFc3ecd7); + final Color onSecondaryContainer = const Color(0xFF476c5b); + final Color onSurface = const Color(0xFF191c1e); + final Color onSurfaceVariant = const Color(0xFF424754); + final Color surfaceContainerHigh = const Color(0xFFe6e8ea); + final Color surfaceContainer = const Color(0xFFeceef0); + + String formatHarga(dynamic value) { + if (value == null) return "0"; + String val = value.toString(); + RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'); + return val.replaceAllMapped(reg, (Match m) => '${m[1]}.'); + } + + final TextEditingController _searchController = TextEditingController(); + List allOrders = []; + List filteredOrders = []; + bool isLoading = true; + Timer? _timer; + int _fetchGeneration = 0; // untuk menghindari race condition saat ganti filter + + @override + void initState() { + super.initState(); + _currentFilter = widget.initialFilter; + fetchOrders(); + _searchController.addListener(_onSearchChanged); + _timer = Timer.periodic(const Duration(seconds: 3), (timer) { + if (mounted) { + fetchOrders(showLoading: false); + } + }); + } + + @override + void dispose() { + _timer?.cancel(); + _searchController.removeListener(_onSearchChanged); + _searchController.dispose(); + super.dispose(); + } + + void _onSearchChanged() { + String query = _searchController.text.toLowerCase(); + setState(() { + filteredOrders = allOrders.where((order) { + String name = (order['customer_name'] ?? "").toString().toLowerCase(); + String orderNum = (order['order_number'] ?? order['id'] ?? "") + .toString() + .toLowerCase(); + return name.contains(query) || orderNum.contains(query); + }).toList(); + }); + } + + String _calculateRemainingTime(String? estimationTime) { + if (estimationTime == null || estimationTime.isEmpty) return "Belum diatur"; + try { + DateTime target = DateTime.parse(estimationTime); + DateTime now = DateTime.now(); + Duration diff = target.difference(now); + if (diff.isNegative) return "Waktu Habis!"; + if (diff.inHours >= 24) return "${(diff.inHours / 24).ceil()} hari lagi"; + String hours = diff.inHours.toString().padLeft(2, '0'); + String minutes = (diff.inMinutes % 60).toString().padLeft(2, '0'); + return "$hours jam $minutes menit lagi"; + } catch (e) { + return "Format error"; + } + } + + Future fetchOrders({bool showLoading = true}) async { + if (!mounted) return; + // Naikkan generation agar response lama bisa diabaikan (race condition fix) + final int myGeneration = ++_fetchGeneration; + if (showLoading) setState(() => isLoading = true); + String baseUrl = AppConfig.baseUrl; + final String? token = widget.userData['access_token']; + // Simpan filter yang sedang aktif saat request dikirim + final String requestedFilter = _currentFilter; + + try { + final response = await http.get( + Uri.parse('$baseUrl/orders/status/${requestedFilter.toUpperCase()}'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + ); + + // Buang response jika ada request yang lebih baru (filter sudah diganti) + if (myGeneration != _fetchGeneration) return; + + if (response.statusCode == 200) { + final Map responseData = json.decode(response.body); + if (mounted) { + setState(() { + List fetchedOrders = responseData['data'] ?? []; + fetchedOrders.sort((a, b) { + int priority(String type) { + String t = type.toLowerCase(); + if (t.contains('kilat')) return 1; + if (t.contains('ekspres') || t.contains('express')) return 2; + return 3; + } + + int pA = priority((a['estimation_type'] ?? '').toString()); + int pB = priority((b['estimation_type'] ?? '').toString()); + if (pA != pB) return pA.compareTo(pB); + + final fallbackTime = DateTime(2100, 1, 1); + DateTime parseTime(dynamic timeStr) { + if (timeStr == null || timeStr.toString().isEmpty) { + return fallbackTime; + } + return DateTime.tryParse(timeStr.toString()) ?? fallbackTime; + } + + DateTime timeA = parseTime(a['estimation_time']); + DateTime timeB = parseTime(b['estimation_time']); + + int comp = timeA.compareTo(timeB); + if (comp != 0) return comp; + return (a['id'] as int) + .compareTo(b['id'] as int); // tertiary sort oldest first + }); + allOrders = fetchedOrders; + // Pertahankan filter pencarian yang sedang aktif + String query = _searchController.text.toLowerCase(); + if (query.isEmpty) { + filteredOrders = allOrders; + } else { + filteredOrders = allOrders.where((order) { + String name = + (order['customer_name'] ?? "").toString().toLowerCase(); + String orderNum = (order['order_number'] ?? order['id'] ?? "") + .toString() + .toLowerCase(); + return name.contains(query) || orderNum.contains(query); + }).toList(); + } + isLoading = false; + }); + } + } else { + if (myGeneration == _fetchGeneration && mounted) { + setState(() => isLoading = false); + } + } + } catch (e) { + if (myGeneration == _fetchGeneration && mounted) { + setState(() => isLoading = false); + } + } + } + + Future updateOrderStatus(int id, + {Map? paymentData}) async { + String baseUrl = AppConfig.baseUrl; + final String? token = widget.userData['access_token']; + try { + final response = await http.put( + Uri.parse('$baseUrl/orders/$id/update-status'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: paymentData != null ? json.encode(paymentData) : null, + ); + if (response.statusCode == 200) fetchOrders(); + } catch (e) { + debugPrint("Update Error: $e"); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: bgBackground, + appBar: _buildAppBar(), + body: Column( + children: [ + _buildSearchBar(), + _buildFilterChips(), + const SizedBox(height: 12), + Expanded( + child: isLoading + ? Center( + child: CircularProgressIndicator( + color: primaryColor, strokeWidth: 3)) + : RefreshIndicator( + onRefresh: fetchOrders, + color: primaryColor, + child: filteredOrders.isEmpty + ? _buildEmptyState() + : ListView.separated( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), + itemCount: filteredOrders.length, + separatorBuilder: (_, __) => + const SizedBox(height: 16), + itemBuilder: (context, i) => + _buildOrderCard(filteredOrders[i]), + ), + ), + ), + ], + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return AppBar( + backgroundColor: surfaceLowest, + elevation: 0, + centerTitle: false, + title: Text("Daftar Pesanan", + style: GoogleFonts.inter( + color: onSurface, fontWeight: FontWeight.w600, fontSize: 18)), + leading: IconButton( + icon: Icon(Icons.arrow_back, color: primaryColor, size: 24), + onPressed: () => Navigator.pop(context), + ), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container(color: surfaceContainerHigh, height: 1.0), + ), + ); + } + + Widget _buildSearchBar() { + return Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), + child: Container( + decoration: BoxDecoration( + color: surfaceLowest, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: outlineVariant.withOpacity(0.3)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 4, + offset: const Offset(0, 2), + ) + ], + ), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: "Cari pelanggan atau no pesanan...", + hintStyle: GoogleFonts.inter(fontSize: 13, color: outlineColor), + prefixIcon: Icon(Icons.search, color: outlineColor, size: 20), + border: InputBorder.none, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + style: GoogleFonts.inter(fontSize: 13), + ), + ), + ); + } + + Widget _buildFilterChips() { + final filters = ['Antrian', 'Proses', 'Selesai']; + return Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 12), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: filters.map((filter) { + bool active = _currentFilter.toLowerCase() == filter.toLowerCase(); + return GestureDetector( + onTap: () { + setState(() => _currentFilter = filter); + fetchOrders(); + }, + child: Container( + margin: const EdgeInsets.only(right: 8), + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + decoration: BoxDecoration( + color: active ? primaryColor : surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + boxShadow: active + ? [ + BoxShadow( + color: primaryColor.withOpacity(0.2), + blurRadius: 12, + offset: const Offset(0, 4)) + ] + : null, + ), + alignment: Alignment.center, + child: Text( + filter, + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w500, + color: active ? Colors.white : onSurfaceVariant, + ), + ), + ), + ); + }).toList(), + ), + ), + ); + } + + Widget _buildOrderCard(dynamic order) { + String title = order['customer_name']?.toString() ?? "Pelanggan"; + + String rawSubtitle = order['service_name']?.toString() ?? "-"; + String formatWeightInString(String input) { + return input.replaceAllMapped(RegExp(r'\((\d+\.?\d*)\s*([a-zA-Z]+)\)'), + (match) { + double val = double.tryParse(match.group(1) ?? "0") ?? 0; + String unit = match.group(2) ?? "Kg"; + if (unit.toUpperCase() == 'PCS') return "(${val.toInt()} $unit)"; + + String valStr = val.toStringAsFixed(3); + + return "($valStr $unit)"; + }); + } + + List serviceItems = rawSubtitle + .split(RegExp(r' \+ |\n')) + .map((e) => formatWeightInString(e)) + .toList(); + + String status = order['status']?.toString().toUpperCase() ?? "PROSES"; + String orderId = "#${order['order_number'] ?? order['id'] ?? 'ACT'}"; + String phone = order['phone']?.toString() ?? "-"; + + String price = "Rp 0"; + if (order['total_price'] != null) { + price = "Rp ${formatHarga(order['total_price'])}"; + } + + String paymentMethod = + (order['payment_method'] ?? "").toString().toUpperCase(); + double cashReceived = + num.tryParse(order['cash_received']?.toString() ?? "0")?.toDouble() ?? + 0.0; + bool isPaid = !(paymentMethod == "BAYAR NANTI" && cashReceived == 0); + + String deliveryType = + order['delivery_type']?.toString().toLowerCase() ?? "none"; + String estimasiTipe = order['estimation_type']?.toString() ?? "Reguler"; + String estimasiWaktu = _calculateRemainingTime(order['estimation_time']); + + if (status == 'MENUNGGU JEMPUT') status = 'PICKUP'; + if (status == 'JEMPUTAN TIBA') status = 'DI TOKO'; + + // Format delivery label + String deliveryLabel = "Drop-off"; + IconData deliveryIcon = Icons.store; + if (deliveryType == 'diantar' || deliveryType == 'delivery') { + deliveryLabel = "Delivery"; + deliveryIcon = Icons.moped; + } else if (deliveryType == 'keduanya' || + deliveryType == 'both' || + deliveryType == 'dijemput' || + deliveryType == 'pickup') { + deliveryLabel = deliveryType == 'dijemput' || deliveryType == 'pickup' + ? "Pick Up" + : "Pick Up & Delivery"; + deliveryIcon = Icons.moped; + } + + bool hasDriverService = deliveryLabel != "Drop-off"; + + Color paymentColor = isPaid ? onSecondaryContainer : errorColor; + Color paymentBg = isPaid ? secondaryContainer : errorColor.withOpacity(0.1); + String paymentText = isPaid ? "LUNAS" : "BELUM BAYAR"; + + String buttonText = "Proses"; + if (status == 'ANTRIAN' || status == 'PICKUP') { + buttonText = "Proses"; + } else if (status == 'PROSES') { + buttonText = "Selesai"; + } else if (status == 'SELESAI') { + buttonText = (deliveryType == 'delivery' || + deliveryType == 'both' || + deliveryType == 'diantar' || + deliveryType == 'keduanya') + ? "Delivery" + : "Diambil"; + } else if (status == 'DI DRIVER') { + buttonText = "Selesai"; + } + + String address = order['address']?.toString() ?? ""; + if (address.isEmpty) address = "Alamat tidak tersedia"; + + return Container( + decoration: BoxDecoration( + color: surfaceLowest, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: outlineVariant.withOpacity(0.5)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 8, + offset: const Offset(0, 2), + ) + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(orderId, + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.bold, + color: primaryColor, + letterSpacing: -0.2)), + const SizedBox(height: 2), + Text(title, + style: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w600, + color: onSurface), + maxLines: 1, + overflow: TextOverflow.ellipsis), + const SizedBox(height: 2), + Row( + children: [ + Icon(Icons.call, size: 14, color: outlineColor), + const SizedBox(width: 4), + Text(phone, + style: GoogleFonts.inter( + fontSize: 11, color: outlineColor)), + ], + ) + ], + ), + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: paymentBg, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: paymentColor.withOpacity(0.2)), + ), + child: Text(paymentText, + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.bold, + color: paymentColor)), + ), + const SizedBox(height: 8), + if (hasDriverService) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: isPaid + ? surfaceContainer + : secondaryContainer.withOpacity(0.5), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(deliveryIcon, + size: 14, + color: isPaid + ? onSurfaceVariant + : onSecondaryContainer), + const SizedBox(width: 4), + Text(deliveryLabel, + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.w600, + color: isPaid + ? onSurfaceVariant + : onSecondaryContainer)), + ], + ), + ) + ], + ) + ], + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: surfaceLow.withOpacity(0.5), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: outlineVariant.withOpacity(0.3)), + ), + child: Column( + children: [ + ...serviceItems.map((item) { + List parts = item.split('||'); + String namePart = parts[0]; + String pricePart = parts.length > 1 + ? "Rp ${formatHarga(parts[1])}" + : ""; + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Icon(Icons.dry_cleaning, + size: 18, color: primaryColor), + const SizedBox(width: 8), + Expanded( + child: Text(namePart, + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + color: onSurface), + maxLines: 1, + overflow: TextOverflow.ellipsis)), + ], + ), + ), + if (pricePart.isNotEmpty) ...[ + const SizedBox(width: 8), + Text(pricePart, + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.bold, + color: primaryColor)), + ] + ], + ), + ); + }), + if ((num.tryParse( + order['delivery_fee']?.toString() ?? "0") ?? + 0) > + 0) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Icon(Icons.two_wheeler, + size: 18, color: Colors.teal.shade700), + const SizedBox(width: 8), + Expanded( + child: Text("Tarif Driver", + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + color: onSurface))), + ], + ), + ), + const SizedBox(width: 8), + Text("Rp ${formatHarga(order['delivery_fee'])}", + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.teal.shade700)), + ], + ), + ), + const SizedBox(height: 10), + Divider( + height: 1, + thickness: 1, + color: outlineVariant.withOpacity(0.3)), + const SizedBox(height: 10), + Row( + children: [ + Row( + children: [ + Icon(Icons.bolt, + size: 14, color: onSurfaceVariant), + const SizedBox(width: 4), + Text(estimasiTipe, + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w600, + color: onSurfaceVariant)), + ], + ), + const SizedBox(width: 16), + Row( + children: [ + Icon(Icons.schedule, + size: 14, + color: estimasiWaktu.contains('Habis') + ? errorColor + : onSurfaceVariant), + const SizedBox(width: 4), + Text(estimasiWaktu, + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w600, + color: estimasiWaktu.contains('Habis') + ? errorColor + : onSurfaceVariant)), + ], + ), + ], + ) + ], + ), + ), + if (hasDriverService) ...[ + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 2), + child: Icon(Icons.location_on, + size: 16, color: outlineColor), + ), + const SizedBox(width: 6), + Expanded( + child: Text(address, + style: GoogleFonts.inter( + fontSize: 11, color: outlineColor), + maxLines: 2, + overflow: TextOverflow.ellipsis), + ) + ], + ) + ] + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: surfaceLowest, + border: Border( + top: BorderSide(color: outlineVariant.withOpacity(0.3))), + borderRadius: + const BorderRadius.vertical(bottom: Radius.circular(16)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("TAGIHAN", + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.w800, + color: outlineColor, + letterSpacing: 1.0)), + Text(price, + style: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w700, + color: onSurface)), + ], + ), + if (status != 'DIAMBIL' && status != 'DITERIMA' && status != 'TIBA DI TOKO') + ElevatedButton( + onPressed: () async { + bool isDelivery = (deliveryType == 'delivery' || + deliveryType == 'both' || + deliveryType == 'diantar' || + deliveryType == 'keduanya'); + + if (status == 'SELESAI' && !isPaid && !isDelivery) { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => HomePaymentPage( + order: order, + userData: widget.userData, + ), + ), + ); + if (result == true) fetchOrders(); + return; + } + + String dialogTitle = "Lanjutkan Pesanan?"; + String dialogContent = + "Apakah Anda yakin ingin melanjutkan pesanan ini ke tahap selanjutnya?"; + + if (status == 'SELESAI') { + if (isDelivery) { + dialogTitle = "Tugaskan ke Driver?"; + dialogContent = + "Pesanan ini akan diteruskan ke tugas Driver untuk diantar. Lanjutkan?"; + } else { + dialogTitle = "Pesanan Diambil?"; + dialogContent = + "Apakah pesanan ini akan diserahkan ke pelanggan? Status akan menjadi Diambil."; + } + } else if (status == 'PROSES') { + dialogTitle = "Selesaikan Cucian?"; + dialogContent = + "Apakah proses cuci sudah selesai dan siap untuk dikemas/diambil?"; + } else if (status == 'ANTRIAN' || status == 'PICKUP') { + dialogTitle = "Mulai Proses?"; + dialogContent = + "Apakah Anda ingin mulai memproses pesanan ini sekarang?"; + } + + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: surfaceLowest, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + title: Text(dialogTitle, + style: GoogleFonts.inter( + fontWeight: FontWeight.w700, + color: onSurface)), + content: Text(dialogContent, + style: + GoogleFonts.inter(color: onSurfaceVariant)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text("Batal", + style: GoogleFonts.inter( + color: outlineColor, + fontWeight: FontWeight.w600)), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8)), + ), + onPressed: () { + Navigator.pop(ctx); + updateOrderStatus(order['id']); + }, + child: Text("Ya, Lanjutkan", + style: GoogleFonts.inter( + fontWeight: FontWeight.w600)), + ), + ], + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + elevation: 0, + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + ), + child: Row( + children: [ + Text(buttonText, + style: GoogleFonts.inter( + fontSize: 12, fontWeight: FontWeight.w600)), + const SizedBox(width: 6), + const Icon(Icons.arrow_forward, size: 16), + ], + ), + ) + ], + ), + ) + ], + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.inventory_2_outlined, + size: 48, color: outlineColor.withOpacity(0.2)), + const SizedBox(height: 16), + Text("Belum ada pesanan", + style: GoogleFonts.inter( + color: outlineColor, fontWeight: FontWeight.w600)), + ], + ), + ); + } +} diff --git a/laundry_mobile/lib/staf/home/payments.dart b/laundry_mobile/lib/staf/home/payments.dart new file mode 100644 index 0000000..1abceb3 --- /dev/null +++ b/laundry_mobile/lib/staf/home/payments.dart @@ -0,0 +1,701 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import '../../config.dart'; +import 'dart:convert'; +import 'package:google_fonts/google_fonts.dart'; +import '../beranda.dart'; +import '../../driver/beranda.dart' as driver_beranda; + +class HomePaymentPage extends StatefulWidget { + final dynamic order; + final Map userData; + + const HomePaymentPage({ + super.key, + required this.order, + required this.userData, + }); + + @override + State createState() => _HomePaymentPageState(); +} + +class _HomePaymentPageState extends State { + // --- Tailwind DESIGN TOKENS --- + final Color primary = const Color(0xFF0058BE); + final Color onPrimary = const Color(0xFFFFFFFF); + final Color primaryContainer = const Color(0xFF2170E4); + final Color background = const Color(0xFFF7F9FB); + final Color surface = const Color(0xFFF7F9FB); + final Color surfaceContainerLowest = const Color(0xFFFFFFFF); + final Color surfaceContainerLow = const Color(0xFFF0F4F8); // changed + final Color onSurface = const Color(0xFF191C1E); + final Color onSurfaceVariant = const Color(0xFF424754); + final Color outlineVariant = const Color(0xFFC2C6D6); + final Color outline = const Color(0xFF727785); + final Color successGreen = const Color(0xFF10B981); + final Color errorRed = const Color(0xFFBA1A1A); + + // State + String selectedMethod = "Tunai"; + bool _isSubmitting = false; + int change = 0; + final TextEditingController _cashReceivedController = TextEditingController(); + + @override + void initState() { + super.initState(); + _cashReceivedController.text = "0"; + _calculateChange("0"); + } + + void _calculateChange(String value) { + int total = widget.order['total_price'] ?? widget.order['total'] ?? 0; + int received = int.tryParse(value.replaceAll(RegExp(r'[^0-9]'), '')) ?? 0; + setState(() { + change = received - total; + }); + } + + String formatHarga(dynamic value) { + if (value == null) return "0"; + String val = value.toString(); + RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'); + return val.replaceAllMapped(reg, (Match m) => '${m[1]}.'); + } + + Future _submitPayment() async { + if (selectedMethod == "Tunai") { + int received = int.tryParse( + _cashReceivedController.text.replaceAll(RegExp(r'[^0-9]'), '')) ?? + 0; + int total = widget.order['total_price'] ?? widget.order['total'] ?? 0; + + if (received <= 0) { + _showSnackBar("Masukkan nominal uang!", errorRed); + return; + } + + if (received < total) { + _showSnackBar( + "Uang tidak cukup! Tagihan: Rp ${formatHarga(total)}", errorRed); + return; + } + } + setState(() => _isSubmitting = true); + try { + String baseUrl = AppConfig.baseUrl; + final String? token = widget.userData['access_token']; + final int orderId = widget.order['id']; + + final response = await http.put( + Uri.parse('$baseUrl/orders/$orderId/update-status'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: json.encode({ + 'payment_method': selectedMethod == "Tunai" ? "CASH" : "QRIS", + 'cash_received': selectedMethod == "Tunai" + ? (int.tryParse(_cashReceivedController.text + .replaceAll(RegExp(r'[^0-9]'), '')) ?? + (widget.order['total_price'] ?? widget.order['total'] ?? 0)) + : (widget.order['total_price'] ?? widget.order['total'] ?? 0), + }), + ); + + if (response.statusCode == 200) { + _showSnackBar("Pelunasan Berhasil!", successGreen); + await Future.delayed(const Duration(seconds: 1)); + if (mounted) { + final String userRole = widget.userData['user']?['role'] ?? 'staf'; + if (userRole == 'driver') { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => + driver_beranda.BerandaPage(userData: widget.userData), + ), + (route) => false, + ); + } else { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => BerandaPage(userData: widget.userData), + ), + (route) => false, + ); + } + } + } else { + _showSnackBar( + "Gagal memproses pelunasan: ${response.statusCode}", errorRed); + } + } catch (e) { + _showSnackBar("Error koneksi: $e", errorRed); + } finally { + if (mounted) setState(() => _isSubmitting = false); + } + } + + void _showSnackBar(String message, Color color) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Icon( + color == successGreen + ? Icons.check_circle_outline + : Icons.error_outline_rounded, + color: Colors.white, + size: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + color == successGreen ? "Berhasil" : "Peringatan", + style: GoogleFonts.inter( + fontWeight: FontWeight.w800, + fontSize: 14, + color: Colors.white), + ), + Text( + message, + style: GoogleFonts.inter( + fontSize: 12, + color: Colors.white.withOpacity(0.9), + fontWeight: FontWeight.w500), + ), + ], + ), + ), + ], + ), + ), + behavior: SnackBarBehavior.floating, + backgroundColor: color.withOpacity(0.95), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: Colors.white.withOpacity(0.2), width: 1), + ), + duration: const Duration(seconds: 3), + )); + } + + String _formatWeightInString(String input) { + return input.replaceAllMapped(RegExp(r'\(([\d\.]+)\s*([a-zA-Z]+)\)'), + (match) { + double val = double.tryParse(match.group(1) ?? "0") ?? 0; + String unit = match.group(2) ?? "Kg"; + if (unit.toUpperCase() == 'PCS') return "(${val.toInt()} $unit)"; + return "(${val.toStringAsFixed(3)} $unit)"; // updated to 3 decimals + }); + } + + @override + Widget build(BuildContext context) { + int total = widget.order['total_price'] ?? widget.order['total'] ?? 0; + + return Scaffold( + backgroundColor: background, + appBar: _buildAppBar(), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildOrderSummary(total), + const SizedBox(height: 24), + _buildPaymentMethods(), + const SizedBox(height: 16), + if (selectedMethod == "Tunai") + _buildCashPanel(total) + else + _buildQRISPanel(), + const SizedBox(height: 40), // extra padding for scrolling + ], + ), + ), + ), + _buildBottomAction(), + ], + ), + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return AppBar( + backgroundColor: Colors.white.withOpacity(0.9), + elevation: 0, + scrolledUnderElevation: 1, + centerTitle: true, + leading: IconButton( + onPressed: () => Navigator.pop(context), + icon: + Icon(Icons.arrow_back_ios_new_rounded, color: onSurface, size: 20), + ), + title: Text("Pelunasan", + style: GoogleFonts.inter( + color: onSurface, fontWeight: FontWeight.w700, fontSize: 18)), + ); + } + + Widget _buildOrderSummary(int total) { + int deliveryFee = + int.tryParse(widget.order['delivery_fee']?.toString() ?? "0") ?? 0; + + // Parse multiple services if needed (like in order_list) + String rawSubtitle = widget.order['service_name']?.toString() ?? + widget.order['subtitle']?.toString() ?? + "-"; + + // Ekstrak ongkir jika berasal dari API dashboard (tergabung di subtitle) + if (deliveryFee == 0 && rawSubtitle.toLowerCase().contains('ongkir')) { + final ongkirMatch = + RegExp(r'Ongkir:\s*Rp\s*([\d\.]+)').firstMatch(rawSubtitle); + if (ongkirMatch != null) { + deliveryFee = + int.tryParse(ongkirMatch.group(1)!.replaceAll('.', '')) ?? 0; + } + } + + String deliveryType = + widget.order['delivery_type']?.toString().toLowerCase() ?? 'none'; + if (deliveryType == 'none' && deliveryFee > 0) { + deliveryType = + 'delivery'; // Asumsi delivery jika ada ongkir tapi tipe kosong + } + + String driverLabel = "Ambil di Toko"; + if (deliveryType == 'pickup' || deliveryType == 'dijemput') { + driverLabel = "Pick-up"; + } else if (deliveryType == 'delivery' || deliveryType == 'diantar') { + driverLabel = "Delivery"; + } else if (deliveryType == 'both' || deliveryType == 'keduanya') { + driverLabel = "Pick-up & Delivery"; + } + + List serviceItems = rawSubtitle + .split(RegExp(r' \+ |\n')) + .where((e) => !e + .toLowerCase() + .contains('ongkir')) // Hapus ongkir dari list layanan + .map((e) => _formatWeightInString(e)) + .toList(); + + return Container( + decoration: BoxDecoration( + color: surfaceContainerLowest, + border: Border.all(color: outlineVariant), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: primary.withOpacity(0.08), + borderRadius: + const BorderRadius.vertical(top: Radius.circular(11)), + border: Border( + bottom: BorderSide(color: outlineVariant.withOpacity(0.5))), + ), + child: Text( + "RINCIAN PESANAN", + textAlign: TextAlign.center, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w700, + color: primary, + letterSpacing: 1.5, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildSummaryRow( + "Pelanggan", + widget.order['customer_name']?.toString() ?? + widget.order['title']?.toString() ?? + "-"), + const SizedBox(height: 12), + _buildSummaryRow("ID Pesanan", + "#${widget.order['order_number'] ?? widget.order['id'] ?? 'ACT'}"), + const SizedBox(height: 16), + Text("Layanan & Harga", + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: onSurfaceVariant)), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: surface, + border: Border.all(color: outlineVariant), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: serviceItems.asMap().entries.map((entry) { + int idx = entry.key; + String item = entry.value; + List parts = item.split('||'); + String namePart = parts[0].trim(); + String pricePart = parts.length > 1 + ? "Rp ${formatHarga(parts[1].trim())}" + : ""; + + // Jika harga per jasa tidak tersedia dari API Beranda, + // hitung otomatis (Total Harga - Ongkos Kirim) jika hanya ada 1 jasa + if (pricePart.isEmpty && serviceItems.length == 1) { + int servicesPrice = total - deliveryFee; + if (servicesPrice > 0) { + pricePart = "Rp ${formatHarga(servicesPrice)}"; + } + } + + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 10), + decoration: BoxDecoration( + border: idx != serviceItems.length - 1 + ? Border( + bottom: BorderSide( + color: outlineVariant.withOpacity(0.5))) + : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text(namePart, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: onSurface))), + Text(pricePart, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: onSurface)), + ], + ), + ); + }).toList(), + ), + ), + const SizedBox(height: 16), + _buildSummaryRow("Opsi Driver", driverLabel), + if (deliveryFee > 0) ...[ + const SizedBox(height: 12), + _buildSummaryRow( + "Ongkos Kirim", "Rp ${formatHarga(deliveryFee)}"), + ], + const SizedBox(height: 16), + _buildDashedLine(), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text("Total Tagihan", + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: primary)), + Text("Rp ${formatHarga(total)}", + style: GoogleFonts.inter( + fontSize: 22, + fontWeight: FontWeight.w700, + color: primary, + letterSpacing: -0.5)), + ], + ), + ], + ), + ) + ], + ), + ); + } + + Widget _buildSummaryRow(String label, String value) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: onSurfaceVariant)), + const SizedBox(width: 16), + Flexible( + child: Text(value, + textAlign: TextAlign.right, + style: GoogleFonts.inter( + fontSize: 14, fontWeight: FontWeight.w600, color: onSurface)), + ), + ], + ); + } + + Widget _buildDashedLine() { + return LayoutBuilder( + builder: (context, constraints) { + final boxWidth = constraints.constrainWidth(); + const dashWidth = 12.0; + const dashHeight = 1.0; + final dashCount = (boxWidth / (2 * dashWidth)).floor(); + return Flex( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + direction: Axis.horizontal, + children: List.generate(dashCount, (_) { + return SizedBox( + width: dashWidth, + height: dashHeight, + child: DecoratedBox( + decoration: BoxDecoration(color: outlineVariant)), + ); + }), + ); + }, + ); + } + + Widget _buildPaymentMethods() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Metode Pelunasan", + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: onSurfaceVariant)), + const SizedBox(height: 12), + Row( + children: [ + _methodOption("Tunai", Icons.payments_outlined), + const SizedBox(width: 16), + _methodOption("QRIS", Icons.qr_code_2_rounded), + ], + ) + ], + ); + } + + Widget _methodOption(String label, IconData icon) { + bool isSelected = selectedMethod == label; + return Expanded( + child: InkWell( + onTap: () => setState(() => selectedMethod = label), + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: isSelected ? primary.withOpacity(0.08) : Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected ? primary : outlineVariant, + width: isSelected ? 1.5 : 1.0), + boxShadow: [ + BoxShadow( + color: isSelected + ? primary.withOpacity(0.15) + : primary.withOpacity(0.04), + blurRadius: 15, + offset: const Offset(0, 5)) + ], + ), + child: Column( + children: [ + Icon(icon, + color: isSelected ? primary : onSurfaceVariant, size: 28), + const SizedBox(height: 8), + Text(label, + style: GoogleFonts.inter( + color: isSelected ? primary : onSurface, + fontWeight: FontWeight.w600, + fontSize: 14)), + ], + ), + ), + ), + ); + } + + Widget _buildCashPanel(int total) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: outlineVariant, width: 1.0), + boxShadow: [ + BoxShadow( + color: primary.withOpacity(0.06), + blurRadius: 20, + offset: const Offset(0, 6)) + ]), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Uang Diterima", + style: GoogleFonts.inter( + color: onSurface, fontWeight: FontWeight.w600)), + Container( + width: 140, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: outlineVariant, width: 1.0)), + child: TextField( + controller: _cashReceivedController, + onChanged: _calculateChange, + keyboardType: TextInputType.number, + textAlign: TextAlign.right, + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + color: primary, + fontSize: 16), + decoration: InputDecoration( + prefixText: "Rp ", + prefixStyle: GoogleFonts.inter( + color: onSurfaceVariant, fontWeight: FontWeight.w500), + border: InputBorder.none, + isDense: true, + contentPadding: const EdgeInsets.symmetric(vertical: 12)), + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Divider(color: outlineVariant, height: 1.0), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Kembalian", + style: GoogleFonts.inter( + color: onSurface, fontWeight: FontWeight.w600)), + Text(change < 0 ? "Rp 0" : "Rp ${formatHarga(change)}", + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + color: change < 0 + ? onSurfaceVariant.withOpacity(0.5) + : successGreen, + fontSize: 18)), + ], + ) + ], + ), + ); + } + + Widget _buildQRISPanel() { + return Container( + width: double.infinity, + margin: const EdgeInsets.only(top: 16), + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: const Color(0xFFF7F9FB), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE6E8EA)), + ), + child: Column( + children: [ + Container( + width: 200, + height: 200, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE6E8EA)), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.asset( + 'assets/qris.png', + fit: BoxFit.contain, + ), + ), + ), + const SizedBox(height: 16), + Text( + "Scan QRIS untuk membayar", + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + color: const Color(0xFF424754), + ), + ), + ], + ), + ); + } + + Widget _buildBottomAction() { + return Container( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + border: Border(top: BorderSide(color: outlineVariant, width: 1.0)), + boxShadow: [ + BoxShadow( + color: primary.withOpacity(0.06), + blurRadius: 24, + offset: const Offset(0, -8)) + ], + ), + child: SafeArea( + child: SizedBox( + width: double.infinity, + height: 54, + child: ElevatedButton( + onPressed: _isSubmitting ? null : _submitPayment, + style: ElevatedButton.styleFrom( + backgroundColor: primary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + elevation: 0, + ), + child: _isSubmitting + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: Colors.white, strokeWidth: 3)) + : Text("SELESAIKAN", + style: GoogleFonts.inter( + color: Colors.white, + fontWeight: FontWeight.w700, + fontSize: 15, + letterSpacing: 0.5)), + ), + ), + ), + ); + } +} diff --git a/laundry_mobile/lib/staf/laundry.dart b/laundry_mobile/lib/staf/laundry.dart new file mode 100644 index 0000000..4b75078 --- /dev/null +++ b/laundry_mobile/lib/staf/laundry.dart @@ -0,0 +1,1503 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import '../config.dart'; +import 'dart:convert'; +import 'dart:async'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import 'laundry/payment.dart'; +import 'profile.dart'; + +class LaundryPage extends StatefulWidget { + final Map userData; + final bool isTab; + final Map? pickupData; + const LaundryPage( + {super.key, required this.userData, this.isTab = false, this.pickupData}); + + @override + State createState() => _LaundryPageState(); +} + +class _LaundryPageState extends State { + // --- Tailwind DESIGN TOKENS --- + final Color primary = const Color(0xFF0058BE); + final Color onPrimary = const Color(0xFFFFFFFF); + final Color primaryContainer = const Color(0xFF2170E4); + final Color primaryLight = const Color(0xFFD8E2FF); + final Color background = const Color(0xFFF7F9FB); + final Color surface = const Color(0xFFF7F9FB); + final Color surfaceContainerLowest = const Color(0xFFFFFFFF); + final Color surfaceContainer = const Color(0xFFECEEF0); + final Color onSurface = const Color(0xFF191C1E); + final Color onSurfaceVariant = const Color(0xFF424754); + final Color outlineVariant = const Color(0xFFC2C6D6); + final Color outline = const Color(0xFF727785); + final Color error = const Color(0xFFBA1A1A); + final Color secondary = const Color(0xFF416656); + final Color tertiary = const Color(0xFF545C72); + final Color successGreen = const Color(0xFF10B981); // for status + final Color surfaceVariant = const Color(0xFFE0E3E5); // added + + // State Data + String? selectedServiceId; + String? selectedEstimationId = "1"; + double weight = 0.0; + int quantity = 1; + int currentItemPrice = 0; + bool isLoading = true; + List services = []; + static List? _cachedServices; + + // Keranjang Layanan + List> cartItems = []; + int grandTotalPrice = 0; + + String selectedDeliveryOption = "Ambil di Toko"; + final List deliveryOptions = ["Ambil di Toko", "Antar ke Rumah"]; + + // Hitung delivery_type otomatis: pickup dari pickupData, delivery dari pilihan + String get deliveryType { + bool hasPickup = widget.pickupData != null; + bool isDelivery = selectedDeliveryOption == "Antar ke Rumah"; + if (hasPickup && isDelivery) return 'both'; + if (hasPickup) return 'pickup'; + if (isDelivery) return 'delivery'; + return 'none'; + } + + final DatabaseReference _dbRef = FirebaseDatabase.instance.ref(); + StreamSubscription? _weightSubscription; + bool isScaleConnected = false; + Timer? _refreshTimer; + + String get _devicePath => 'laundry'; + + bool get _isEstimationActive { + final active = widget.userData['shop']?['is_estimation_active']; + if (active == null) return false; + return active == 1 || active == true; + } + + bool get _shouldShowEstimation { + if (selectedServiceId == null) return false; + return _isEstimationActive && + !isSatuanService() && + (_expressExtraPrice > 0 || _kilatExtraPrice > 0); + } + + int get _expressExtraPrice { + final price = widget.userData['shop']?['express_extra_price']; + return price != null ? int.tryParse(price.toString()) ?? 0 : 0; + } + + int get _kilatExtraPrice { + final price = widget.userData['shop']?['kilat_extra_price']; + return price != null ? int.tryParse(price.toString()) ?? 0 : 0; + } + + final List> estimations = [ + { + "id": "1", + "name": "Reguler (3 Hari)", + "multiplier": 1.0, + "desc": "Normal" + }, + {"id": "2", "name": "Express (24 Jam)", "multiplier": 1.5, "desc": "Cepat"}, + {"id": "3", "name": "Kilat (6 Jam)", "multiplier": 2.0, "desc": "Priority"}, + ]; + + String formatHarga(dynamic value) { + if (value == null) return "0"; + String val = value.toString(); + RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'); + return val.replaceAllMapped(reg, (Match m) => '${m[1]}.'); + } + + void _checkAndInitScale() { + if (_weightSubscription == null) { + _dbRef.child('$_devicePath/current_weight').set(0.0); + _dbRef.child('$_devicePath/status_hp').set("-|-|-|-|Rp0"); + _listenToScale(); + } + } + + @override + void initState() { + super.initState(); + if (_cachedServices != null) { + services = _cachedServices!; + isLoading = false; + } + fetchServices(showLoading: _cachedServices == null); + _checkAndInitScale(); + _refreshTimer = Timer.periodic(const Duration(seconds: 3), (_) { + if (mounted) fetchServices(showLoading: false); + }); + } + + void _listenToScale() { + if (_devicePath.isEmpty) return; + _weightSubscription = + _dbRef.child('$_devicePath/current_weight').onValue.listen((event) { + if (event.snapshot.value != null && mounted) { + setState(() { + isScaleConnected = true; + double newWeight = + double.tryParse(event.snapshot.value.toString()) ?? 0.0; + + if (newWeight > 20.0) newWeight = 20.0; + if (newWeight < 0.1) newWeight = 0.0; + + if (!isSatuanService()) { + weight = newWeight; + _calculateCurrentPrice(); + } + }); + } + }); + } + + void _calculateCurrentPrice() { + if (selectedServiceId == null) return; + final dynamic service; + try { + service = + services.firstWhere((s) => s['id'].toString() == selectedServiceId); + } catch (e) { + return; + } + + double basePrice = double.tryParse(service['price'].toString()) ?? 0.0; + bool isSatuan = isSatuanService(); + double amount = isSatuan + ? quantity.toDouble() + : double.parse(weight.toStringAsFixed(2)); + + String estId = _shouldShowEstimation ? (selectedEstimationId ?? "1") : "1"; + double extraPrice = 0; + if (estId == "2") { + extraPrice = _expressExtraPrice.toDouble(); + } else if (estId == "3") { + extraPrice = _kilatExtraPrice.toDouble(); + } + + setState(() { + double rawPrice = (basePrice * amount) + (extraPrice * amount); + currentItemPrice = _roundTo500(rawPrice.round()); + }); + + //String namaJasa = service['name'].toString(); + //if (namaJasa.length > 8) namaJasa = namaJasa.substring(0, 8); + //String textUntukOLED = + //"$namaJasa|${est['name'].toString().split(' ')[0]}|${isSatuan ? 'Satuan' : 'Beban'}|${isSatuan ? '$quantity p' : '${weight.toStringAsFixed(3)} kg'}|Rp$currentItemPrice"; + //_dbRef.child('laundry/status_hp').set(textUntukOLED); + _updateOLEDDisplay(); + } + + void _addItemToCart() { + if (selectedServiceId == null) return; + if (!isSatuanService() && weight <= 0) { + _showSnackBar("Berat belum terdeteksi dari timbangan!", Colors.orange); + return; + } + + final service = + services.firstWhere((s) => s['id'].toString() == selectedServiceId); + + String estId = _shouldShowEstimation ? (selectedEstimationId ?? "1") : "1"; + final est = estimations.firstWhere((e) => e['id'].toString() == estId, + orElse: () => estimations.first); + + String getCategory(String name) { + name = name.toLowerCase(); + if (name.contains('express') || name.contains('ekspres')) return 'Express'; + if (name.contains('kilat')) return 'Kilat'; + return 'Reguler'; + } + + String newItemCategory = getCategory(_shouldShowEstimation ? est['name'].toString() : ""); + + if (cartItems.isNotEmpty) { + String firstItemCategory = getCategory(cartItems.first['estimation_name'].toString()); + if (newItemCategory != firstItemCategory) { + _showSnackBar( + "Gagal: Layanan $newItemCategory tidak bisa digabung dengan layanan $firstItemCategory. Mohon buat nota terpisah.", + Colors.red); + return; + } + } + + setState(() { + cartItems.add({ + 'service_id': selectedServiceId, + 'service_name': service['name'], + 'estimation_name': + _shouldShowEstimation ? est['name'].toString().split(' ')[0] : "", + 'unit': service['unit'], + 'weight': isSatuanService() ? quantity.toDouble() : weight, + 'price': currentItemPrice, + }); + _calculateGrandTotal(); + _resetInputForm(); + }); + } + + void _calculateGrandTotal() { + int total = 0; + for (var item in cartItems) { + total += (item['price'] as int); + } + setState(() { + grandTotalPrice = _roundTo500(total); + }); + } + + /// Membulatkan harga ke atas kelipatan 500 + int _roundTo500(int value) { + if (value <= 0) return 0; + return ((value / 500).ceil() * 500).toInt(); + } + + void _removeItem(int index) { + setState(() { + cartItems.removeAt(index); + _calculateGrandTotal(); + _updateOLEDDisplay(); // <--- Tambahkan baris ini + }); + } + + void _resetInputForm() { + setState(() { + selectedServiceId = null; + weight = 0.0; + quantity = 1; + currentItemPrice = 0; + }); + // Panggil fungsi update untuk mencerminkan status keranjang saat ini + _updateOLEDDisplay(); + } + + void _updateOLEDDisplay() { + List listJasa = []; + List listBerat = []; + int totalHarga = grandTotalPrice; + + // 1. Ambil SEMUA riwayat pesanan dari keranjang (Cart) + for (var item in cartItems) { + listJasa.add(item['service_name'].toString()); + if (item['unit'] == 'PCS') { + listBerat.add("${(item['weight'] as num).toInt()} pcs"); + } else { + listBerat.add("${(item['weight'] as num).toStringAsFixed(3)} kg"); + } + } + + // 2. Tambah jasa yang SEDANG DIPILIH di form (jika ada) + // TAPI: Hanya jika user sedang memilih jasa (selectedServiceId != null) + if (selectedServiceId != null) { + try { + final service = + services.firstWhere((s) => s['id'].toString() == selectedServiceId); + listJasa.add(service['name'].toString()); + totalHarga += + currentItemPrice; // Tambahkan harga item yang sedang dipilih + + if (service['unit'] == 'PCS') { + listBerat.add("$quantity pcs"); + } else { + listBerat + .add(weight > 0 ? "${weight.toStringAsFixed(3)} kg" : "0 kg"); + } + } catch (e) { + // ignore: empty_catches + } + } + + // 3. Gabungkan semua jadi String + String strJasa = listJasa.isNotEmpty ? listJasa.join(", ") : "-"; + if (strJasa.length > 50) { + strJasa = + "${strJasa.substring(0, 47)}..."; // Membatasi string agar memori OLED/ESP32 tidak lambat + } + String strBerat = listBerat.isNotEmpty ? listBerat.join(", ") : "0"; + String strEst = "-"; + if (selectedServiceId != null || cartItems.isNotEmpty) { + strEst = "Reguler"; // Default ke Reguler jika ada barang + } + + if (selectedServiceId != null && _shouldShowEstimation) { + String estId = selectedEstimationId ?? "1"; + strEst = estimations.firstWhere((e) => e['id'].toString() == estId, orElse: () => estimations.first)['name'].toString().split(' ')[0]; + } else if (cartItems.isNotEmpty) { + bool hasKilat = cartItems.any((item) => item['estimation_name']?.toString().toUpperCase() == 'KILAT'); + bool hasExpress = cartItems.any((item) => item['estimation_name']?.toString().toUpperCase() == 'EXPRESS'); + + if (hasKilat) { + strEst = "Kilat"; + } else if (hasExpress) { + strEst = "Express"; + } + } + + // Menentukan tipe layanan dinamis (Satuan / Beban) + String tipeLayanan = "Beban"; + if (selectedServiceId != null) { + tipeLayanan = isSatuanService() ? "Satuan" : "Beban"; + } else if (cartItems.isNotEmpty) { + bool hasKiloan = cartItems.any((item) => item['unit'] != 'PCS'); + tipeLayanan = hasKiloan ? "Beban" : "Satuan"; + } + + // 4. Kirim ke Firebase + String textUntukOLED = + "$strJasa|$strEst|$tipeLayanan|$strBerat|Rp${_roundTo500(totalHarga)}"; + if (_devicePath.isNotEmpty) { + _dbRef.child('$_devicePath/status_hp').set(textUntukOLED); + } + } + + @override + void dispose() { + _refreshTimer?.cancel(); + _weightSubscription?.cancel(); + super.dispose(); + } + + bool isSatuanService() { + if (selectedServiceId == null) return false; + try { + final service = + services.firstWhere((s) => s['id'].toString() == selectedServiceId); + return service['unit']?.toString().toUpperCase() == 'PCS'; + } catch (e) { + return false; + } + } + + Future fetchServices({bool showLoading = true}) async { + if (!mounted) return; + if (showLoading) setState(() => isLoading = true); + try { + final response = + await http.get(Uri.parse("${AppConfig.baseUrl}/services"), headers: { + 'Authorization': 'Bearer ${widget.userData['access_token']}', + }); + if (response.statusCode == 200) { + final decoded = json.decode(response.body); + _cachedServices = (decoded is Map && decoded.containsKey('data')) + ? decoded['data'] + : decoded; + setState(() { + services = _cachedServices!; + isLoading = false; + }); + } + } catch (e) { + if (mounted && _cachedServices == null) setState(() => isLoading = false); + } + } + + @override + Widget build(BuildContext context) { + _checkAndInitScale(); + bool isSatuan = isSatuanService(); + + Widget content = isLoading + ? Center(child: CircularProgressIndicator(color: primary)) + : Column( + children: [ + if (widget.pickupData == null) _buildAppBar(), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB( + 20, 24, 20, 100), // added bottom padding + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.pickupData != null + ? "Input Pesanan Pick Up" + : "Buat Pesanan", + style: GoogleFonts.inter( + fontSize: 28, + fontWeight: FontWeight.w700, + color: onSurface, + letterSpacing: -0.02)), + const SizedBox(height: 4), + Text( + widget.pickupData != null + ? "Selesaikan data pesanan dari layanan penjemputan." + : "Pilih layanan laundry untuk pesanan pelanggan.", + style: GoogleFonts.inter( + fontSize: 16, + color: onSurfaceVariant, + fontWeight: FontWeight.w400)), + const SizedBox(height: 24), + + if (widget.pickupData != null) ...[ + _buildPickupCustomerCard(), + const SizedBox(height: 24), + ], + + _buildSectionTitle("Pilih Layanan"), + const SizedBox(height: 12), + _buildServiceDropdown(), + + const SizedBox(height: 24), + if (_shouldShowEstimation) ...[ + _buildSectionTitle("Estimasi Waktu"), + const SizedBox(height: 12), + _buildEstimationCards(), + const SizedBox(height: 24), + ], + if (selectedServiceId != null) ...[ + _buildSectionTitle("Detail Input"), + const SizedBox(height: 12), + _buildInputDisplay(isSatuan), + const SizedBox(height: 16), + _buildAddButton(), + const SizedBox(height: 32), + ], + if (cartItems.isNotEmpty) ...[ + _buildSectionTitle("Keranjang"), + const SizedBox(height: 12), + _buildCartList(), + const SizedBox(height: 32), + ], + _buildSectionTitle("Metode Pengiriman"), + const SizedBox(height: 12), + _buildDeliveryOptions(), + const SizedBox(height: 32), + + // No longer need _buildPaymentSummaryCard here, we will show it dynamically above or floating if needed. Wait, HTML puts summary inside Cart. + + if (widget.isTab) const SizedBox(height: 100), + ], + ), + ), + ), + ], + ); + + return Scaffold( + backgroundColor: background, + body: Stack( + children: [ + content, + if (cartItems.isNotEmpty) + Positioned( + bottom: (widget.isTab || widget.pickupData != null) + ? 20 + : 0, // HTML uses fixed bottom-24 floating button + left: 20, + right: 20, + child: _buildFloatingPayButton(), + ), + ], + ), + bottomNavigationBar: + isLoading || widget.isTab || widget.pickupData != null + ? null + : _buildModernBottomBar(), + ); + } + + // --- WIDGET COMPONENTS --- + + Widget _buildPickupCustomerCard() { + final order = widget.pickupData!; + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: surfaceContainerLowest, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: outlineVariant.withOpacity(0.5)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 4), + ) + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.person_pin_circle_rounded, color: primary, size: 24), + const SizedBox(width: 8), + Text("Informasi Pelanggan", + style: GoogleFonts.inter( + fontSize: 15, + fontWeight: FontWeight.w700, + color: onSurface)), + ], + ), + const SizedBox(height: 16), + _infoRow(Icons.person_outline_rounded, "Nama", + order['customer_name'] ?? '-'), + const SizedBox(height: 8), + _infoRow(Icons.phone_outlined, "Telepon", order['phone'] ?? '-'), + const SizedBox(height: 8), + _infoRow( + Icons.location_on_outlined, "Alamat", order['address'] ?? '-', + maxLines: 2), + ], + ), + ); + } + + Widget _infoRow(IconData icon, String label, String value, + {int maxLines = 1}) { + return Row( + crossAxisAlignment: + maxLines > 1 ? CrossAxisAlignment.start : CrossAxisAlignment.center, + children: [ + Icon(icon, size: 16, color: onSurfaceVariant), + const SizedBox(width: 8), + SizedBox( + width: 70, + child: Text(label, + style: GoogleFonts.inter(fontSize: 13, color: onSurfaceVariant)), + ), + const Text(": "), + Expanded( + child: Text(value, + maxLines: maxLines, + overflow: maxLines > 1 ? TextOverflow.ellipsis : null, + style: GoogleFonts.inter( + fontSize: 13, fontWeight: FontWeight.w600, color: onSurface)), + ), + ], + ); + } + + Widget _buildAppBar() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: surface.withOpacity(0.9), + border: Border( + bottom: BorderSide( + color: const Color(0xFFE6E8EA) + .withOpacity(0.5), // surfaceContainerHigh + width: 1.0, + ), + ), + ), + child: SafeArea( + bottom: false, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 32, + height: 32, + decoration: const BoxDecoration( + color: Color(0xFFD8E2FF), // primaryFixed + shape: BoxShape.circle, + ), + clipBehavior: Clip.hardEdge, + child: Image.asset( + 'assets/app_icon.png', + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Icon( + Icons.local_laundry_service_rounded, + color: primary, + size: 20), + ), + ), + const SizedBox(width: 8), + Text("My Laundry", + style: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w600, + color: primary, + )), + ], + ), + const SizedBox(width: 48), // Empty space for alignment + ], + ), + ), + ); + } + + Widget _buildSectionTitle(String title) { + return Text(title, + style: GoogleFonts.inter( + fontSize: 16, fontWeight: FontWeight.w600, color: onSurface)); + } + + Widget _buildServiceDropdown() { + final dynamic selectedService = selectedServiceId == null + ? null + : services.firstWhere((s) => s['id'].toString() == selectedServiceId, + orElse: () => null); + + return GestureDetector( + onTap: _showServicePicker, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: surfaceContainerLowest, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: selectedServiceId != null + ? primary + : outlineVariant.withOpacity(0.5), + width: 1.0), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(12)), + child: Icon(Icons.local_laundry_service_rounded, + color: primary, size: 24), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + selectedService != null + ? selectedService['name'] + : "Pilih Layanan Laundry", + style: GoogleFonts.inter( + fontSize: 15, + fontWeight: FontWeight.w600, + color: selectedService != null + ? onSurface + : onSurfaceVariant), + ), + if (selectedService != null) + Text( + "Rp ${formatHarga(selectedService['price'])} / ${selectedService['unit']}", + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: primary), + ) + else + Text( + "Ketuk untuk melihat daftar layanan", + style: GoogleFonts.inter( + fontSize: 12, + color: onSurfaceVariant.withOpacity(0.8), + fontWeight: FontWeight.w400), + ), + ], + ), + ), + Icon(Icons.keyboard_arrow_down_rounded, + color: onSurfaceVariant, size: 24), + ], + ), + ), + ); + } + + void _showServicePicker() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) => Container( + height: MediaQuery.of(context).size.height * 0.7, + decoration: BoxDecoration( + color: background, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + children: [ + const SizedBox(height: 12), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: outlineVariant, + borderRadius: BorderRadius.circular(2)), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + children: [ + Text("Pilih Layanan", + style: GoogleFonts.inter( + fontSize: 20, + fontWeight: FontWeight.w700, + color: onSurface)), + const Spacer(), + IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon(Icons.close_rounded, color: onSurfaceVariant), + style: IconButton.styleFrom( + backgroundColor: surfaceContainerLowest), + ) + ], + ), + ), + Expanded( + child: ListView.separated( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 40), + itemCount: services.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final s = services[index]; + bool isSelected = selectedServiceId == s['id'].toString(); + + Color iconBg = primary.withOpacity(0.1); + Color iconColor = primary; + IconData iconData = Icons.local_laundry_service_rounded; + if (s['unit'].toString().toUpperCase() == 'PCS') { + iconBg = tertiary.withOpacity(0.1); + iconColor = tertiary; + iconData = Icons.dry_cleaning; + } else if (s['name'] + .toString() + .toLowerCase() + .contains('setrika')) { + iconBg = primary.withOpacity(0.1); + iconColor = primary; + iconData = Icons.iron; + } else { + iconBg = secondary.withOpacity(0.1); + iconColor = secondary; + iconData = Icons.local_laundry_service; + } + + return GestureDetector( + onTap: () { + setState(() { + selectedServiceId = s['id'].toString(); + _calculateCurrentPrice(); + }); + Navigator.pop(context); + }, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: surfaceContainerLowest, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? primary + : outlineVariant.withOpacity(0.5), + width: isSelected ? 1.5 : 1.0), + boxShadow: isSelected + ? [ + BoxShadow( + color: primary.withOpacity(0.1), + blurRadius: 4) + ] + : null, + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: isSelected ? iconColor : iconBg, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + iconData, + color: isSelected ? onPrimary : iconColor, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s['name'], + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + fontSize: 14, + color: onSurface), + ), + Text( + s['unit'].toString().toUpperCase() == 'PCS' + ? 'Perawatan premium' + : 'Bersih & harum', + style: GoogleFonts.inter( + fontSize: 11, color: onSurfaceVariant), + ), + ], + ), + ), + Text( + "Rp ${formatHarga(s['price'])}/${s['unit'].toString().toLowerCase() == 'kg' ? 'kg' : 'pc'}", + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + fontSize: 14, + color: primary), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ), + ); + } + + Widget _buildEstimationCards() { + // Filter estimasi berdasarkan konfigurasi harga tambahan di pengaturan + final availableEstimations = estimations.where((est) { + if (est['id'].toString() == "1") return true; // Reguler selalu tampil + if (est['id'].toString() == "2") return _expressExtraPrice > 0; + if (est['id'].toString() == "3") return _kilatExtraPrice > 0; + return true; + }).toList(); + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: availableEstimations.map((est) { + bool isSelected = selectedEstimationId == est['id'].toString(); + + // Memberi warna aksen berbeda untuk tiap estimasi + Color estColor; + if (est['id'].toString() == "1") { + estColor = successGreen; + } else if (est['id'].toString() == "2") { + estColor = error; + } else { + estColor = error; + } + + return GestureDetector( + onTap: () => setState(() { + selectedEstimationId = est['id'].toString(); + _calculateCurrentPrice(); + }), + child: Container( + width: 140, + margin: const EdgeInsets.only(right: 12, bottom: 4), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isSelected + ? estColor.withOpacity(0.08) + : surfaceContainerLowest, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: + isSelected ? estColor : outlineVariant.withOpacity(0.3), + width: 1.0, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Icon( + isSelected + ? Icons.bolt_rounded + : Icons.schedule_rounded, + color: isSelected ? estColor : onSurfaceVariant), + Container( + width: 22, + height: 22, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected ? estColor : Colors.transparent, + border: Border.all( + color: isSelected ? estColor : outlineVariant, + width: 1.5), + ), + child: isSelected + ? const Icon(Icons.check, + size: 14, color: Colors.white) + : null, + ) + ], + ), + const SizedBox(height: 12), + Text(est['name'].toString().split(' ')[0], + style: GoogleFonts.inter( + fontWeight: FontWeight.w700, + fontSize: 15, + color: isSelected ? estColor : onSurface)), + Text( + est['name'] + .toString() + .replaceAll(est['name'].toString().split(' ')[0], '') + .trim(), + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w500, + color: isSelected ? estColor : onSurfaceVariant)), + const SizedBox(height: 12), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: isSelected + ? estColor + : outlineVariant.withOpacity(0.5), + borderRadius: BorderRadius.circular(8), + ), + child: Text(est['desc'], + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.w700, + color: isSelected ? Colors.white : onSurfaceVariant, + letterSpacing: 0.5)), + ) + ], + ), + ), + ); + }).toList(), + ), + ); + } + + Widget _buildInputDisplay(bool isSatuan) { + // Variasi warna untuk membedakan mode satuan dan mode kiloan + Color inputAccentColor = isSatuan ? error : primary; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: surfaceContainerLowest, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: outlineVariant.withOpacity(0.3), width: 1.0), + ), + child: Column( + children: [ + isSatuan + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: inputAccentColor.withOpacity(0.1), + border: Border.all( + color: inputAccentColor.withOpacity(0.3), + width: 1.0), + borderRadius: BorderRadius.circular(12)), + child: Icon(Icons.checkroom_rounded, + color: inputAccentColor), + ), + const SizedBox(width: 16), + Text("Jumlah Pcs", + style: GoogleFonts.inter( + fontWeight: FontWeight.w700, + fontSize: 15, + color: onSurface)), + ], + ), + Container( + decoration: BoxDecoration( + color: background, + border: Border.all(color: outlineVariant, width: 1.0), + borderRadius: BorderRadius.circular(30)), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.remove, size: 20), + color: inputAccentColor, + onPressed: () { + if (quantity > 1) { + setState(() { + quantity--; + _calculateCurrentPrice(); + }); + } + }, + ), + SizedBox( + width: 24, + child: Text("$quantity", + textAlign: TextAlign.center, + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + fontSize: 16))), + IconButton( + icon: const Icon(Icons.add, size: 20), + color: Colors.white, + style: IconButton.styleFrom( + backgroundColor: inputAccentColor), + onPressed: () { + setState(() { + quantity++; + _calculateCurrentPrice(); + }); + }, + ), + ], + ), + ) + ], + ) + : Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: inputAccentColor.withOpacity(0.1), + border: Border.all( + color: inputAccentColor.withOpacity(0.3), + width: 1.0), + borderRadius: BorderRadius.circular(12)), + child: Icon(Icons.scale_rounded, color: inputAccentColor), + ), + const SizedBox(width: 16), + Expanded( + child: Text("Berat Pakaian", + style: GoogleFonts.inter( + fontWeight: FontWeight.w700, + fontSize: 15, + color: onSurface)), + ), + Text("${weight.toStringAsFixed(3)} kg", + style: GoogleFonts.inter( + fontSize: 24, + fontWeight: FontWeight.w600, + color: inputAccentColor, + letterSpacing: -0.5)), + ], + ), + const SizedBox(height: 16), + Divider(color: outlineVariant, height: 1.0), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Subtotal", + style: GoogleFonts.inter( + color: onSurfaceVariant, + fontWeight: FontWeight.w500, + fontSize: 14)), + Text("Rp ${formatHarga(currentItemPrice)}", + style: GoogleFonts.inter( + color: primary, + fontWeight: FontWeight.w700, + fontSize: 18)), + ], + ) + ], + ), + ); + } + + Widget _buildAddButton() { + return SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: primary.withOpacity(0.1), + foregroundColor: primary, + elevation: 0, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: _addItemToCart, + icon: const Icon(Icons.add_circle_outline, size: 20), + label: Text("Tambahkan Item Ini", + style: + GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 14)), + ), + ); + } + + Widget _buildCartList() { + int subtotal = + cartItems.fold(0, (sum, item) => sum + (item['price'] as int)); + int totalEstimasi = subtotal; // Delivery fee is set later + + return Container( + decoration: BoxDecoration( + color: surfaceContainerLowest, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: outlineVariant.withOpacity(0.5), width: 1.0), + boxShadow: [ + BoxShadow( + color: onSurface.withOpacity(0.02), + blurRadius: 4, + offset: const Offset(0, 2)) + ], + ), + child: Column( + children: [ + ListView.separated( + padding: const EdgeInsets.all(12), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: cartItems.length, + separatorBuilder: (_, __) => + Divider(color: outlineVariant.withOpacity(0.3), height: 24), + itemBuilder: (context, index) { + final item = cartItems[index]; + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(item['service_name'], + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + color: onSurface, + fontSize: 14)), + Text( + item['estimation_name'].toString().isNotEmpty + ? "${item['estimation_name']} โ€ข ${item['unit'] == 'PCS' ? item['weight'].toInt() : item['weight'].toStringAsFixed(3)} ${item['unit']}" + : "${item['unit'] == 'PCS' ? item['weight'].toInt() : item['weight'].toStringAsFixed(3)} ${item['unit']}", + style: GoogleFonts.inter( + color: onSurfaceVariant, + fontSize: 11, + fontWeight: FontWeight.w400)), + ], + ), + ), + Text("Rp ${formatHarga(item['price'])}", + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + color: onSurface, + fontSize: 14)), + const SizedBox(width: 12), + InkWell( + onTap: () => _removeItem(index), + borderRadius: BorderRadius.circular(20), + child: Container( + width: 28, + height: 28, + decoration: BoxDecoration( + border: Border.all(color: outlineVariant), + shape: BoxShape.circle), + child: + Icon(Icons.remove, color: onSurfaceVariant, size: 16), + ), + ) + ], + ); + }, + ), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: surfaceContainer.withOpacity(0.5), + border: Border( + top: BorderSide(color: outlineVariant.withOpacity(0.3))), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Total Estimasi", + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: onSurface)), + Text("Rp ${formatHarga(_roundTo500(totalEstimasi))}", + style: GoogleFonts.inter( + fontSize: 20, + fontWeight: FontWeight.w600, + color: primary)), + ], + ), + ], + ), + ) + ], + ), + ); + } + + Widget _buildDeliveryOptions() { + return Row( + children: deliveryOptions.map((option) { + bool isSelected = selectedDeliveryOption == option; + + return Expanded( + child: GestureDetector( + onTap: () => setState(() => selectedDeliveryOption = option), + child: Container( + margin: EdgeInsets.only( + right: option == deliveryOptions.first ? 6 : 0, + left: option == deliveryOptions.last ? 6 : 0), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + decoration: BoxDecoration( + color: isSelected + ? primary.withOpacity(0.05) + : surfaceContainerLowest, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? primary : outlineVariant.withOpacity(0.3), + width: 1.0, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + option == "Ambil di Toko" + ? Icons.store + : Icons.moped_rounded, + color: isSelected ? primary : onSurfaceVariant, + size: 24), + const SizedBox(height: 4), + Text( + option, + textAlign: TextAlign.center, + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isSelected ? primary : onSurface, + ), + ), + ], + ), + ), + ), + ); + }).toList(), + ); + } + + Widget _buildModernBottomBar() { + return Container( + decoration: BoxDecoration( + color: surfaceContainerLowest, + border: Border(top: BorderSide(color: surfaceVariant.withOpacity(0.5))), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _navItem(Icons.home_outlined, "Beranda", false, () { + if (_devicePath.isNotEmpty) { + _dbRef.child('$_devicePath/screen_status').set("OFF"); + } + Navigator.pop(context); + }), + _navItem(Icons.add_circle, "Input", true, () {}), + _navItem(Icons.person_outline, "Profil", false, () { + if (_devicePath.isNotEmpty) { + _dbRef.child('$_devicePath/screen_status').set("OFF"); + } + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => + ProfilePage(userData: widget.userData))); + }), + ], + ), + ), + ), + ); + } + + Widget _navItem( + IconData icon, String label, bool isActive, VoidCallback onTap) { + if (isActive) { + return InkWell( + onTap: onTap, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Icon(icon, color: primary, size: 24), + ), + const SizedBox(height: 4), + Text(label, + style: GoogleFonts.inter( + fontSize: 12, fontWeight: FontWeight.w600, color: primary)), + ], + ), + ); + } + + return InkWell( + onTap: onTap, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 24, color: onSurfaceVariant), + const SizedBox(height: 4), + Text(label, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w500, + color: onSurfaceVariant)), + ], + ), + ); + } + + Widget _buildFloatingPayButton() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: primary.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4)) + ], + ), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: primary, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + elevation: 0, + ), + onPressed: () { + double totalWeight = cartItems.fold( + 0, (sum, item) => sum + (item['weight'] as double)); + + String estType = "Reguler"; + if (cartItems + .every((e) => e['estimation_name'].toString().trim().isEmpty)) { + estType = "None"; + } else if (cartItems.any((e) => e['estimation_name'] + .toString() + .toLowerCase() + .contains('kilat'))) { + estType = "Kilat"; + } else if (cartItems.any((e) => + e['estimation_name'] + .toString() + .toLowerCase() + .contains('ekspres') || + e['estimation_name'] + .toString() + .toLowerCase() + .contains('express'))) { + estType = "Ekspres"; + } + + int totalBill = _roundTo500( + cartItems.fold(0, (sum, item) => sum + (item['price'] as int))); + + // Bersihkan item yang menggantung (tidak jadi di-add) agar OLED update hanya sesuai isi keranjang valid + _resetInputForm(); + + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => PaymentPage( + totalBill: totalBill, + cartItems: cartItems, + userData: widget.userData, + weight: totalWeight, + deliveryType: deliveryType, + estimationType: estType, + pickupData: widget.pickupData, + ), + )).then((result) { + if (result == true) { + setState(() { + cartItems = []; + grandTotalPrice = 0; + _resetInputForm(); + }); + } + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Proses Pesanan", + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + fontSize: 16, + color: onPrimary)), + ], + ), + ), + ); + } + + void _showSnackBar(String msg, Color bg) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Icon( + bg == successGreen + ? Icons.check_circle_outline + : Icons.error_outline_rounded, + color: Colors.white, + size: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + bg == successGreen ? "Berhasil" : "Peringatan", + style: GoogleFonts.inter( + fontWeight: FontWeight.w800, + fontSize: 14, + color: Colors.white), + ), + Text( + msg, + style: GoogleFonts.inter( + fontSize: 12, + color: Colors.white.withOpacity(0.9), + fontWeight: FontWeight.w500), + ), + ], + ), + ), + ], + ), + ), + behavior: SnackBarBehavior.floating, + backgroundColor: bg.withOpacity(0.95), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: Colors.white.withOpacity(0.2), width: 1), + ), + duration: const Duration(seconds: 3), + )); + } +} diff --git a/laundry_mobile/lib/staf/laundry/map.dart b/laundry_mobile/lib/staf/laundry/map.dart new file mode 100644 index 0000000..8846f60 --- /dev/null +++ b/laundry_mobile/lib/staf/laundry/map.dart @@ -0,0 +1,507 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'dart:async'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:ui' as ui; // Solusi untuk mengatasi bentrok Path +import 'package:shared_preferences/shared_preferences.dart'; + +class MapPickerPage extends StatefulWidget { + final LatLng? initialLocation; + const MapPickerPage({super.key, this.initialLocation}); + + @override + State createState() => _MapPickerPageState(); +} + +class _MapPickerPageState extends State { + // --- Tailwind DESIGN TOKENS --- + final Color primary = const Color(0xFF0058BE); + final Color onPrimary = const Color(0xFFFFFFFF); + final Color primaryContainer = const Color(0xFF2170E4); + final Color background = const Color(0xFFF7F9FB); + final Color surface = const Color(0xFFF7F9FB); + final Color surfaceContainerLowest = const Color(0xFFFFFFFF); + final Color surfaceContainerLow = const Color(0xFFF0F4F8); + final Color onSurface = const Color(0xFF191C1E); + final Color onSurfaceVariant = const Color(0xFF424754); + final Color outlineVariant = const Color(0xFFC2C6D6); + final Color outline = const Color(0xFF727785); + + LatLng _selectedLocation = + const LatLng(-6.200000, 106.816666); // Default Jakarta + final MapController _mapController = MapController(); + bool _isLoading = true; + + // Search variables + final TextEditingController _searchController = TextEditingController(); + List _searchResults = []; + Timer? _debounce; + + @override + void initState() { + super.initState(); + if (widget.initialLocation != null) { + _selectedLocation = widget.initialLocation!; + } else { + _loadShopLocation(); + } + _isLoading = false; + } + + Future _loadShopLocation() async { + try { + final prefs = await SharedPreferences.getInstance(); + final userStr = prefs.getString('userData'); + if (userStr != null) { + final userData = json.decode(userStr); + final shop = userData['shop']; + if (shop != null) { + final lat = shop['shop_latitude'] ?? shop['latitude']; + final lng = shop['shop_longitude'] ?? shop['longitude']; + if (lat != null && lng != null) { + setState(() { + _selectedLocation = LatLng( + double.parse(lat.toString()), double.parse(lng.toString())); + _mapController.move(_selectedLocation, 15.0); + }); + } + } + } + } catch (e) { + debugPrint("Gagal memuat lokasi toko: $e"); + } + } + + @override + void dispose() { + _searchController.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + Future _determinePosition() async { + bool serviceEnabled; + LocationPermission permission; + + setState(() => _isLoading = true); + + serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + setState(() => _isLoading = false); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Layanan lokasi (GPS) tidak aktif. Silakan aktifkan terlebih dahulu.'))); + } + return; + } + + permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + setState(() => _isLoading = false); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Izin lokasi ditolak oleh pengguna.'))); + } + return; + } + } + + if (permission == LocationPermission.deniedForever) { + setState(() => _isLoading = false); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Izin lokasi ditolak permanen. Buka pengaturan untuk mengizinkan.'), + action: SnackBarAction( + label: 'PENGATURAN', + textColor: Colors.white, + onPressed: () { + Geolocator.openAppSettings(); + }, + ), + ), + ); + } + return; + } + + try { + Position position = await Geolocator.getCurrentPosition( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.high, + timeLimit: Duration(seconds: 10), + ), + ); + if (mounted) { + setState(() { + _selectedLocation = LatLng(position.latitude, position.longitude); + _isLoading = false; + }); + _mapController.move(_selectedLocation, 16.0); + } + } catch (e) { + if (mounted) { + setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Gagal mendapatkan lokasi saat ini: $e'))); + } + } + } + + Future _searchAddress(String query) async { + if (query.isEmpty) { + setState(() { + _searchResults = []; + }); + return; + } + + try { + final response = await http.get( + Uri.parse( + 'https://nominatim.openstreetmap.org/search?q=$query&format=json&limit=5&addressdetails=1'), + headers: {'User-Agent': 'laundry_mobile_app'}, + ); + + if (response.statusCode == 200) { + setState(() { + _searchResults = json.decode(response.body); + }); + } + // ignore: empty_catches + } catch (e) {} + } + + void _onSearchChanged(String query) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 500), () { + _searchAddress(query); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: background, + appBar: AppBar( + backgroundColor: Colors.white.withOpacity(0.95), + elevation: 0, + scrolledUnderElevation: 1, + centerTitle: true, + leading: IconButton( + onPressed: () => Navigator.pop(context), + icon: + Icon(Icons.arrow_back_ios_new_rounded, color: onSurface, size: 20), + ), + title: Text("Pilih Lokasi Alamat", + style: GoogleFonts.inter( + color: onSurface, fontWeight: FontWeight.w700, fontSize: 16)), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container(color: outlineVariant, height: 1.0), + ), + ), + body: Stack( + children: [ + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: _selectedLocation, + initialZoom: 15, + onPositionChanged: (position, hasGesture) { + if (hasGesture) { + setState(() { + _selectedLocation = position.center; + }); + } + }, + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'com.example.laundry_mobile', + ), + ], + ), + + // Search Bar + Positioned( + top: 20, + left: 20, + right: 20, + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: outlineVariant, width: 2.0), + boxShadow: [ + BoxShadow( + color: primary.withOpacity(0.06), + blurRadius: 20, + offset: const Offset(0, 8), + ) + ], + ), + child: TextField( + controller: _searchController, + onChanged: _onSearchChanged, + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + color: onSurface, + fontSize: 14), + decoration: InputDecoration( + hintText: "Cari alamat atau nama tempat...", + hintStyle: GoogleFonts.inter( + color: onSurfaceVariant.withOpacity(0.6), + fontWeight: FontWeight.w500, + fontSize: 14), + prefixIcon: Icon(Icons.search_rounded, + color: primary, size: 22), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: Icon(Icons.cancel_rounded, + size: 20, color: onSurfaceVariant), + onPressed: () { + _searchController.clear(); + setState(() => _searchResults = []); + }, + ) + : null, + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + + // Hasil Pencarian + if (_searchResults.isNotEmpty) + Container( + margin: const EdgeInsets.only(top: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: outlineVariant, width: 2.0), + boxShadow: [ + BoxShadow( + color: primary.withOpacity(0.06), + blurRadius: 20, + offset: const Offset(0, 8), + ) + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(14), + child: ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: _searchResults.length, + separatorBuilder: (context, index) => + Divider(height: 1, color: outlineVariant), + itemBuilder: (context, index) { + final item = _searchResults[index]; + return ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 4), + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: outlineVariant)), + child: Icon(Icons.location_on_rounded, + size: 20, color: primary), + ), + title: Text( + item['display_name'], + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w500, + color: onSurface), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + onTap: () { + final lat = double.parse(item['lat']); + final lon = double.parse(item['lon']); + final newPos = LatLng(lat, lon); + setState(() { + _selectedLocation = newPos; + _searchResults = []; + _searchController.text = item['display_name']; + }); + _mapController.move(newPos, 16.0); + FocusScope.of(context).unfocus(); + }, + ); + }, + ), + ), + ), + ], + ), + ), + + // Center Marker + Center( + child: Padding( + padding: const EdgeInsets.only(bottom: 35), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: primary, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: primary.withOpacity(0.4), + blurRadius: 12, + offset: const Offset(0, 6)) + ], + border: Border.all(color: Colors.white, width: 3)), + child: const Icon(Icons.location_on_rounded, + color: Colors.white, size: 28), + ), + // Segitiga kecil dibawah marker + ClipPath( + clipper: _TriangleClipper(), + child: Container( + width: 14, + height: 10, + color: primary, + ), + ) + ], + ), + ), + ), + + // Loading Overlay + if (_isLoading) + Container( + color: Colors.white.withOpacity(0.8), + child: + Center(child: CircularProgressIndicator(color: primary)), + ), + + // UI Buttons Bottom + Positioned( + bottom: 24, + left: 20, + right: 20, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Tombol My Location + InkWell( + onTap: _determinePosition, + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: outlineVariant, width: 2.0), + boxShadow: [ + BoxShadow( + color: primary.withOpacity(0.06), + blurRadius: 15, + offset: const Offset(0, 5), + ) + ], + ), + child: Icon(Icons.my_location_rounded, + color: primary, size: 24), + ), + ), + const SizedBox(height: 16), + + // Panel Konfirmasi + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: outlineVariant, width: 2.0), + boxShadow: [ + BoxShadow( + color: primary.withOpacity(0.08), + blurRadius: 24, + offset: const Offset(0, 10), + ) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Geser peta untuk menentukan titik alamat", + textAlign: TextAlign.center, + style: GoogleFonts.inter( + color: onSurfaceVariant, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + height: 54, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: primary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + ), + onPressed: () { + Navigator.pop(context, _selectedLocation); + }, + child: Text( + "KONFIRMASI LOKASI", + style: GoogleFonts.inter( + color: Colors.white, + fontWeight: FontWeight.w700, + fontSize: 15, + letterSpacing: 0.5, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +// Menggunakan ui.Path agar tidak tertukar dengan Path milik latlong2 +class _TriangleClipper extends CustomClipper { + @override + ui.Path getClip(Size size) { + final path = ui.Path(); + path.lineTo(size.width, 0.0); + path.lineTo(size.width / 2, size.height); + path.close(); + return path; + } + + @override + bool shouldReclip(_TriangleClipper oldClipper) => false; +} diff --git a/laundry_mobile/lib/staf/laundry/payment.dart b/laundry_mobile/lib/staf/laundry/payment.dart new file mode 100644 index 0000000..b20889e --- /dev/null +++ b/laundry_mobile/lib/staf/laundry/payment.dart @@ -0,0 +1,954 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import '../../config.dart'; +import 'dart:convert'; +import 'dart:async'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'map.dart'; +import '../beranda.dart'; + +class PaymentPage extends StatefulWidget { + final int totalBill; + final List cartItems; + final Map userData; + final double weight; + final String deliveryType; + final String estimationType; + final Map? pickupData; + + const PaymentPage({ + super.key, + required this.totalBill, + required this.cartItems, + required this.userData, + required this.weight, + required this.deliveryType, + required this.estimationType, + this.pickupData, + }); + + @override + State createState() => _PaymentPageState(); +} + +class _PaymentPageState extends State { + // --- Tailwind DESIGN TOKENS --- + final Color primary = const Color(0xFF0058BE); + final Color onPrimary = const Color(0xFFFFFFFF); + final Color primaryContainer = const Color(0xFF2170E4); + final Color background = const Color(0xFFF7F9FB); + final Color surface = const Color(0xFFF7F9FB); + final Color surfaceContainerLowest = const Color(0xFFFFFFFF); + final Color surfaceContainerLow = const Color(0xFFF0F4F8); + final Color onSurface = const Color(0xFF191C1E); + final Color onSurfaceVariant = const Color(0xFF424754); + final Color outlineVariant = const Color(0xFFC2C6D6); + final Color outline = const Color(0xFF727785); + final Color successGreen = const Color(0xFF10B981); + final Color errorRed = const Color(0xFFBA1A1A); + + // State + String selectedMethod = "Tunai"; + bool _isSubmitting = false; + // ignore: unused_field + bool _isLoadingFee = false; + int change = 0; + int _deliveryFee = 0; + + String formatHarga(dynamic value) { + if (value == null) return "0"; + String val = value.toString(); + RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'); + return val.replaceAllMapped(reg, (Match m) => '${m[1]}.'); + } + + /// Membulatkan harga ke atas kelipatan 500 + int _roundTo500(int value) { + return ((value / 500).ceil() * 500).toInt(); + } + + // Controllers + final TextEditingController _cashReceivedController = TextEditingController(); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _phoneController = TextEditingController(); + final TextEditingController _addressController = TextEditingController(); + + LatLng? _selectedCoordinates; + + @override + void initState() { + super.initState(); + _cashReceivedController.text = "0"; + _calculateChange("0"); + + if (widget.pickupData != null) { + _nameController.text = widget.pickupData!['customer_name'] ?? ''; + _phoneController.text = widget.pickupData!['phone'] ?? ''; + _addressController.text = widget.pickupData!['address'] ?? ''; + + if (widget.pickupData!['latitude'] != null && + widget.pickupData!['longitude'] != null) { + _selectedCoordinates = LatLng( + double.tryParse(widget.pickupData!['latitude'].toString()) ?? 0.0, + double.tryParse(widget.pickupData!['longitude'].toString()) ?? 0.0, + ); + _checkDeliveryFee(_selectedCoordinates!); + } + } + } + + Future _checkDeliveryFee(LatLng loc) async { + setState(() => _isLoadingFee = true); + try { + final String? token = widget.userData['access_token']; + final response = await http.post( + Uri.parse("${AppConfig.baseUrl}/check-delivery-fee"), + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": "Bearer $token", + }, + body: json.encode({ + "latitude": loc.latitude, + "longitude": loc.longitude, + "delivery_type": widget.deliveryType, + }), + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + setState(() { + _deliveryFee = + _roundTo500((data['delivery_fee'] as num?)?.toInt() ?? 0); + }); + _calculateChange(_cashReceivedController.text); + } + } catch (e) { + debugPrint("Error check delivery fee: $e"); + } finally { + if (mounted) setState(() => _isLoadingFee = false); + } + } + + void _calculateChange(String value) { + int received = int.tryParse(value.replaceAll(RegExp(r'[^0-9]'), '')) ?? 0; + int roundedTotal = _roundTo500(widget.totalBill + _deliveryFee); + setState(() { + change = received - roundedTotal; + if (change < 0) change = 0; + }); + } + + Future _submitTransaction() async { + if (_nameController.text.isEmpty || _phoneController.text.isEmpty) { + _showSnackBar("Nama dan Nomor WA wajib diisi!", Colors.orange); + return; + } + + if (widget.deliveryType != 'none' && _addressController.text.isEmpty) { + _showSnackBar( + "Alamat lengkap wajib diisi untuk layanan driver!", Colors.orange); + return; + } + + if (selectedMethod == "Tunai") { + int received = int.tryParse( + _cashReceivedController.text.replaceAll(RegExp(r'[^0-9]'), '')) ?? + 0; + int roundedTotal = _roundTo500(widget.totalBill + _deliveryFee); + + if (received <= 0) { + _showSnackBar("Masukkan nominal uang!", Colors.orange); + return; + } + + if (received < roundedTotal) { + _showSnackBar( + "Uang tidak cukup! Tagihan: Rp ${formatHarga(roundedTotal)}", + Colors.red); + return; + } + } + + setState(() => _isSubmitting = true); + try { + final String? token = widget.userData['access_token']; + final response = await http.post( + Uri.parse("${AppConfig.baseUrl}/transactions"), + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": "Bearer $token", + }, + body: json.encode({ + "customer_name": _nameController.text.trim(), + "phone": _phoneController.text.trim(), + "address": widget.deliveryType != 'none' + ? _addressController.text.trim() + : null, + "items": widget.cartItems + .map((item) => { + "service_id": item['service_id'], + "service_name": item['service_name'], + "price": item['price'], // Note: this is subtotal price + "qty_or_weight": item['weight'], + "unit": item['unit'], + "subtotal": item['price'], + "estimation_name": item['estimation_name'], + }) + .toList(), + "total_weight": widget.weight, + "total_price": _roundTo500(widget.totalBill), + "latitude": _selectedCoordinates?.latitude, + "longitude": _selectedCoordinates?.longitude, + "status": "ANTRIAN", + "payment_method": selectedMethod == "Tunai" + ? "CASH" + : (selectedMethod == "QRIS" ? "QRIS" : "BAYAR NANTI"), + "estimation_type": widget.estimationType, + "delivery_type": widget.deliveryType, + if (widget.pickupData != null && widget.pickupData!['id'] != null) + "order_id": widget.pickupData!['id'], + "cash_received": selectedMethod == "Tunai" + ? (int.tryParse(_cashReceivedController.text + .replaceAll(RegExp(r'[^0-9]'), '')) ?? + widget.totalBill) + : (selectedMethod == "QRIS" ? widget.totalBill : 0), + "cash_change": selectedMethod == "Tunai" ? change : 0, + }), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + _showSnackBar("Transaksi Berhasil & WA Terkirim!", successGreen); + + try { + final dbRef = FirebaseDatabase.instance.ref(); + await dbRef.child('laundry/screen_status').set("RESTART"); + } catch(e) { + debugPrint("Failed to send RESTART command: $e"); + } + + await Future.delayed(const Duration(seconds: 1)); + if (mounted) { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => BerandaPage(userData: widget.userData), + ), + (route) => false, + ); + } + } else { + _showSnackBar( + "Gagal simpan transaksi: ${response.statusCode}", Colors.red); + } + } catch (e) { + _showSnackBar("Error koneksi: $e", Colors.red); + } finally { + if (mounted) setState(() => _isSubmitting = false); + } + } + + void _showSnackBar(String message, Color color) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Icon( + color == successGreen + ? Icons.check_circle_outline + : Icons.error_outline_rounded, + color: Colors.white, + size: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + color == successGreen ? "Berhasil" : "Peringatan", + style: GoogleFonts.inter( + fontWeight: FontWeight.w800, + fontSize: 14, + color: Colors.white), + ), + Text( + message, + style: GoogleFonts.inter( + fontSize: 12, + color: Colors.white.withOpacity(0.9), + fontWeight: FontWeight.w500), + ), + ], + ), + ), + ], + ), + ), + behavior: SnackBarBehavior.floating, + backgroundColor: color.withOpacity(0.95), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: Colors.white.withOpacity(0.2), width: 1), + ), + duration: const Duration(seconds: 3), + )); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: background, + appBar: _buildModernAppBar(), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildBillCard(), + const SizedBox(height: 32), + _buildSectionHeader("Data Pelanggan"), + const SizedBox(height: 12), + _buildCustomerForm(), + if (widget.deliveryType != 'none') ...[ + const SizedBox(height: 24), + _buildSectionHeader("Alamat"), + const SizedBox(height: 12), + _buildAddressForm(), + ], + const SizedBox(height: 24), + _buildSectionHeader("Metode Pembayaran"), + const SizedBox(height: 12), + _buildPaymentMethodSection(), + const SizedBox(height: 40), + ], + ), + ), + bottomNavigationBar: _buildBottomAction(), + ); + } + + // --- WIDGET COMPONENTS --- + + PreferredSizeWidget _buildModernAppBar() { + return AppBar( + backgroundColor: Colors.white.withOpacity(0.95), + elevation: 0, + scrolledUnderElevation: 1, + centerTitle: true, + leading: IconButton( + onPressed: () => Navigator.pop(context), + icon: + Icon(Icons.arrow_back_ios_new_rounded, color: onSurface, size: 20), + ), + title: Text("Konfirmasi Bayar", + style: GoogleFonts.inter( + color: onSurface, fontWeight: FontWeight.w700, fontSize: 18)), + ); + } + + Widget _buildSectionHeader(String title) { + return Text(title, + style: GoogleFonts.inter( + color: onSurface, fontSize: 16, fontWeight: FontWeight.w700)); + } + + Widget _buildBillCard() { + // Tentukan label layanan driver + String driverLabel; + switch (widget.deliveryType) { + case 'pickup': + driverLabel = 'Pick-up'; + break; + case 'delivery': + driverLabel = 'Delivery'; + break; + case 'both': + driverLabel = 'Pick-up & Delivery'; + break; + default: + driverLabel = 'Ambil di Toko'; + } + + return Container( + decoration: BoxDecoration( + color: surfaceContainerLowest, + border: Border.all(color: outlineVariant), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: primary.withOpacity(0.08), + borderRadius: + const BorderRadius.vertical(top: Radius.circular(11)), + border: Border( + bottom: BorderSide(color: outlineVariant.withOpacity(0.5))), + ), + child: Text( + "RINCIAN PESANAN", + textAlign: TextAlign.center, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w700, + color: primary, + letterSpacing: 1.5, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text("Layanan & Harga", + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: onSurfaceVariant)), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: surface, + border: Border.all(color: outlineVariant), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: widget.cartItems.asMap().entries.map((entry) { + int idx = entry.key; + var item = entry.value; + String qtyStr = item['unit'] == 'PCS' + ? "${item['weight'].toInt()} PCS" + : "${item['weight'].toStringAsFixed(3)} KG"; + String namePart = "${item['service_name']} ($qtyStr)"; + String pricePart = "Rp ${formatHarga(item['price'])}"; + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 10), + decoration: BoxDecoration( + border: idx != widget.cartItems.length - 1 + ? Border( + bottom: BorderSide( + color: outlineVariant.withOpacity(0.5))) + : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text(namePart, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: onSurface))), + Text(pricePart, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: onSurface)), + ], + ), + ); + }).toList(), + ), + ), + const SizedBox(height: 16), + _buildReceiptRow("Opsi Pengiriman", driverLabel), + if (widget.deliveryType != 'none') ...[ + const SizedBox(height: 12), + _buildReceiptRow( + "Tarif Driver", + _selectedCoordinates == null + ? "Pilih Lokasi" + : (_deliveryFee > 0 + ? "Rp ${formatHarga(_deliveryFee)}" + : "Gratis Ongkir"), + isHighlight: + _deliveryFee == 0 && _selectedCoordinates != null, + ), + ], + const SizedBox(height: 16), + _buildDashedLine(), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text("Total Tagihan", + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: primary)), + Text( + "Rp ${formatHarga(_roundTo500(widget.totalBill + _deliveryFee))}", + style: GoogleFonts.inter( + fontSize: 22, + fontWeight: FontWeight.w700, + color: primary, + letterSpacing: -0.5)), + ], + ), + ], + ), + ) + ], + ), + ); + } + + Widget _buildReceiptRow(String label, String value, + {bool isHighlight = false}) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, + style: GoogleFonts.inter( + color: onSurfaceVariant, + fontSize: 12, + fontWeight: FontWeight.w600)), + const SizedBox(width: 16), + Flexible( + child: Text(value, + textAlign: TextAlign.right, + style: GoogleFonts.inter( + color: isHighlight ? successGreen : onSurface, + fontSize: 14, + fontWeight: FontWeight.w600)), + ), + ], + ); + } + + Widget _buildDashedLine() { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final boxWidth = constraints.constrainWidth(); + const dashWidth = 5.0; + const dashHeight = 1.0; + final dashCount = (boxWidth / (2 * dashWidth)).floor(); + return Flex( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + direction: Axis.horizontal, + children: List.generate(dashCount, (_) { + return SizedBox( + width: dashWidth, + height: dashHeight, + child: DecoratedBox( + decoration: BoxDecoration(color: outlineVariant), + ), + ); + }), + ); + }, + ); + } + + Widget _buildCustomerForm() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: outlineVariant, width: 1.0), // Dipertipis + boxShadow: [ + BoxShadow( + color: primary.withOpacity(0.06), // Diperlembut + blurRadius: 20, + offset: const Offset(0, 6)) + ], + ), + child: Column( + children: [ + _buildInputField( + "Nama Pelanggan", Icons.person_outline_rounded, _nameController), + const SizedBox(height: 16), + _buildInputField( + "Nomor WhatsApp", Icons.phone_outlined, _phoneController, + type: TextInputType.phone), + ], + ), + ); + } + + Widget _buildAddressForm() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: outlineVariant, width: 1.0), // Dipertipis + boxShadow: [ + BoxShadow( + color: primary.withOpacity(0.06), // Diperlembut + blurRadius: 20, + offset: const Offset(0, 6)) + ], + ), + child: Column( + children: [ + TextField( + controller: _addressController, + maxLines: 3, + style: GoogleFonts.inter( + color: onSurface, fontWeight: FontWeight.w500, fontSize: 14), + decoration: InputDecoration( + hintText: "Masukkan alamat lengkap beserta patokan...", + hintStyle: GoogleFonts.inter( + color: onSurfaceVariant.withOpacity(0.5), + fontWeight: FontWeight.w400), + filled: true, + fillColor: background, + contentPadding: const EdgeInsets.all(16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: outlineVariant, width: 1.0)), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: outlineVariant, width: 1.0)), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: primary, width: 1.0)), + ), + ), + const SizedBox(height: 16), + InkWell( + onTap: () async { + final result = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => const MapPickerPage()), + ); + if (result != null && result is LatLng) { + setState(() { + _selectedCoordinates = result; + }); + _checkDeliveryFee(result); + } + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), + decoration: BoxDecoration( + color: _selectedCoordinates != null + ? successGreen.withOpacity(0.1) + : background, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _selectedCoordinates != null + ? successGreen + : outlineVariant, + width: 1.0, // Dipertipis + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _selectedCoordinates != null + ? Icons.location_on_rounded + : Icons.map_outlined, + size: 20, + color: + _selectedCoordinates != null ? successGreen : onSurface, + ), + const SizedBox(width: 8), + Text( + _selectedCoordinates != null + ? "Lokasi Sudah Disematkan" + : "Sematkan Lokasi di Map", + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + color: _selectedCoordinates != null + ? successGreen + : onSurface, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildInputField( + String hint, IconData icon, TextEditingController ctrl, + {TextInputType type = TextInputType.text}) { + return TextField( + controller: ctrl, + keyboardType: type, + style: GoogleFonts.inter( + color: onSurface, fontWeight: FontWeight.w500, fontSize: 14), + decoration: InputDecoration( + prefixIcon: Icon(icon, color: onSurfaceVariant, size: 22), + hintText: hint, + hintStyle: GoogleFonts.inter( + color: onSurfaceVariant.withOpacity(0.5), + fontWeight: FontWeight.w400), + filled: true, + fillColor: background, + contentPadding: const EdgeInsets.symmetric(vertical: 16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: + BorderSide(color: outlineVariant, width: 1.0)), // Dipertipis + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: outlineVariant, width: 1.0)), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: primary, width: 1.0)), + ), + ); + } + + Widget _buildPaymentMethodSection() { + return Column( + children: [ + Row( + children: [ + _methodOption("Tunai", Icons.payments_outlined), + const SizedBox(width: 8), + _methodOption("QRIS", Icons.qr_code_2_rounded), + const SizedBox(width: 8), + _methodOption("Bayar Nanti", Icons.history_rounded), + ], + ), + const SizedBox(height: 16), + if (selectedMethod == "Tunai") + _buildCashInput() + else if (selectedMethod == "QRIS") + _buildQRISView() + else + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.05), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.orange.withOpacity(0.2))), + child: Row( + children: [ + const Icon(Icons.info_outline_rounded, color: Colors.orange), + const SizedBox(width: 12), + Expanded( + child: Text( + "Pembayaran akan ditagihkan saat cucian diambil di toko atau saat diantar oleh Driver.", + style: GoogleFonts.inter( + fontSize: 13, + color: Colors.orange.shade900, + fontWeight: FontWeight.w500)), + ), + ], + ), + ), + ], + ); + } + + Widget _methodOption(String label, IconData icon) { + bool isSelected = selectedMethod == label; + return Expanded( + child: InkWell( + onTap: () => setState(() => selectedMethod = label), + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: isSelected ? primary.withOpacity(0.08) : Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected ? primary : outlineVariant, + width: isSelected ? 1.5 : 1.0), // Dipertipis + boxShadow: [ + BoxShadow( + color: isSelected + ? primary.withOpacity(0.15) + : primary.withOpacity(0.04), // Diperlembut + blurRadius: 15, + offset: const Offset(0, 5)) + ], + ), + child: Column( + children: [ + Icon(icon, + color: isSelected ? primary : onSurfaceVariant, size: 28), + const SizedBox(height: 8), + Text(label, + style: GoogleFonts.inter( + color: isSelected ? primary : onSurface, + fontWeight: FontWeight.w600, + fontSize: 14)), + ], + ), + ), + ), + ); + } + + Widget _buildCashInput() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: outlineVariant, width: 1.0), // Dipertipis + boxShadow: [ + BoxShadow( + color: primary.withOpacity(0.06), // Diperlembut + blurRadius: 20, + offset: const Offset(0, 6)) + ]), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Uang Diterima", + style: GoogleFonts.inter( + color: onSurface, fontWeight: FontWeight.w600)), + Container( + width: 140, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: outlineVariant, width: 1.0)), // Dipertipis + child: TextField( + controller: _cashReceivedController, + onChanged: _calculateChange, + keyboardType: TextInputType.number, + textAlign: TextAlign.right, + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + color: primary, + fontSize: 16), + decoration: InputDecoration( + prefixText: "Rp ", + prefixStyle: GoogleFonts.inter( + color: onSurfaceVariant, fontWeight: FontWeight.w500), + border: InputBorder.none, + isDense: true, + contentPadding: const EdgeInsets.symmetric(vertical: 12)), + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Divider(color: outlineVariant, height: 1.0), // Dipertipis + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Kembalian", + style: GoogleFonts.inter( + color: onSurface, fontWeight: FontWeight.w600)), + Text("Rp ${formatHarga(change)}", + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + color: successGreen, + fontSize: 18)), + ], + ), + ], + ), + ); + } + + Widget _buildQRISView() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: outlineVariant, width: 1.0), + boxShadow: [ + BoxShadow( + color: primary.withOpacity(0.06), + blurRadius: 20, + offset: const Offset(0, 6)) + ]), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: outlineVariant, width: 1.0)), + child: Image.asset( + 'assets/qris.png', + width: 160, + fit: BoxFit.contain, + ), + ), + const SizedBox(height: 16), + Text("Scan QR untuk pembayaran", + style: GoogleFonts.inter( + color: onSurfaceVariant, + fontWeight: FontWeight.w500, + fontSize: 13)), + ], + ), + ); + } + + Widget _buildBottomAction() { + return Container( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + border: Border( + top: BorderSide(color: outlineVariant, width: 1.0)), // Dipertipis + boxShadow: [ + BoxShadow( + color: primary.withOpacity(0.06), // Diperlembut + blurRadius: 24, + offset: const Offset(0, -8)) + ], + ), + child: SafeArea( + child: SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: _isSubmitting ? null : _submitTransaction, + style: ElevatedButton.styleFrom( + backgroundColor: primary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + elevation: 0, + ), + child: _isSubmitting + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: Colors.white, strokeWidth: 3)) + : Text("SIMPAN TRANSAKSI", + style: GoogleFonts.inter( + fontWeight: FontWeight.w700, + fontSize: 15, + letterSpacing: 0.5)), + ), + ), + ), + ); + } +} diff --git a/laundry_mobile/lib/staf/profile.dart b/laundry_mobile/lib/staf/profile.dart new file mode 100644 index 0000000..740372d --- /dev/null +++ b/laundry_mobile/lib/staf/profile.dart @@ -0,0 +1,342 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:ui' as ui; +import 'laundry.dart'; +import '../login.dart'; + +class ProfilePage extends StatefulWidget { + final Map userData; + final bool isTab; + const ProfilePage({super.key, required this.userData, this.isTab = false}); + + @override + State createState() => _ProfilePageState(); +} + +class _ProfilePageState extends State { + // --- Tailwind DESIGN TOKENS --- + final Color primary = const Color(0xFF0058BE); + final Color onPrimary = const Color(0xFFFFFFFF); + final Color primaryContainer = const Color(0xFF2170E4); + final Color background = const Color(0xFFF7F9FB); + final Color surface = const Color(0xFFF7F9FB); + final Color surfaceContainerLowest = const Color(0xFFFFFFFF); + final Color surfaceContainerLow = const Color(0xFFF0F4F8); + final Color onSurface = const Color(0xFF191C1E); + final Color onSurfaceVariant = const Color(0xFF424754); + final Color outlineVariant = const Color(0xFFC2C6D6); + final Color outline = const Color(0xFF727785); + final Color error = const Color(0xFFBA1A1A); + final Color primaryFixed = const Color(0xFFD8E2FF); + final Color surfaceContainerHigh = const Color(0xFFE6E8EA); + final Color successGreen = const Color(0xFF10B981); + final Color dangerRed = const Color(0xFFBA1A1A); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + String displayName = widget.userData['user']?['name'] ?? 'User'; + String email = widget.userData['user']?['email'] ?? "Tidak ada email"; + + Widget content = Column( + children: [ + _buildAppBar(), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildProfileHero(displayName, email), + const SizedBox(height: 24), + _buildMenuList(context), + const SizedBox(height: 32), + _buildVersionFooter(), + const SizedBox(height: 24), + ], + ), + ), + ), + ], + ); + + if (widget.isTab) return content; + + return Scaffold( + backgroundColor: surface, + body: content, + bottomNavigationBar: _buildBottomNav(context), + ); + } + + Widget _buildAppBar() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: surface.withOpacity(0.9), + border: Border( + bottom: BorderSide( + color: const Color(0xFFE6E8EA).withOpacity(0.5), + width: 1.0, + ), + ), + ), + child: SafeArea( + bottom: false, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 32, + height: 32, + decoration: const BoxDecoration( + color: Color(0xFFD8E2FF), + shape: BoxShape.circle, + ), + clipBehavior: Clip.hardEdge, + child: Image.asset( + 'assets/app_icon.png', + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Icon( + Icons.local_laundry_service_rounded, + color: primary, + size: 20), + ), + ), + const SizedBox(width: 8), + Text("My Laundry", + style: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w600, + color: primary, + )), + ], + ), + const SizedBox(width: 48), + ], + ), + ), + ); + } + + Widget _buildProfileHero(String name, String email) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF2170E4), Color(0xFF004395)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2)) + ], + ), + child: Stack( + children: [ + Positioned( + right: -16, + top: -16, + child: Container( + width: 96, + height: 96, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withOpacity(0.05), + ), + ), + ), + Row( + children: [ + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withOpacity(0.3), width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), blurRadius: 2) + ], + ), + child: CircleAvatar( + backgroundColor: Colors.white, + child: Icon(Icons.person, color: primary, size: 40), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white, + letterSpacing: -0.5), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + email, + style: GoogleFonts.inter( + fontSize: 13, color: Colors.white.withOpacity(0.9)), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ) + ], + ) + ], + ), + ); + } + + Widget _buildMenuList(BuildContext context) { + return Column( + children: [ + _buildLogoutButton(context), + ], + ); + } + + Widget _buildLogoutButton(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('userData'); + if (!context.mounted) return; + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const LoginPage()), + (route) => false); + }, + borderRadius: BorderRadius.circular(12), + highlightColor: error.withOpacity(0.1), + splashColor: error.withOpacity(0.05), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + border: Border.all(color: outlineVariant.withOpacity(0.3)), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.logout, color: error, size: 20), + const SizedBox(width: 8), + Text( + "Keluar", + style: GoogleFonts.inter( + fontSize: 14, fontWeight: FontWeight.w500, color: error), + ), + ], + ), + ), + ), + ); + } + + Widget _buildVersionFooter() { + return Center( + child: Text( + "", + textAlign: TextAlign.center, + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w500, + letterSpacing: 0.5, + color: onSurfaceVariant.withOpacity(0.4), + ), + ), + ); + } + + Widget _buildBottomNav(BuildContext context) { + return ClipRect( + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + decoration: BoxDecoration( + color: surface.withOpacity(0.9), + border: + Border(top: BorderSide(color: outlineVariant.withOpacity(0.1))), + ), + child: SafeArea( + child: SizedBox( + height: 64, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _navItem(Icons.home_rounded, "Beranda", false, () { + Navigator.popUntil(context, (route) => route.isFirst); + }), + _navItem(Icons.add_circle, "Input", false, () { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => + LaundryPage(userData: widget.userData)), + ); + }), + _navItem(Icons.person_rounded, "Profil", true, () {}), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _navItem( + IconData icon, String label, bool isActive, VoidCallback onTap) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + child: SizedBox( + width: 64, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, + size: 24, color: isActive ? primary : onSurfaceVariant), + const SizedBox(height: 2), + Text( + label, + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: isActive ? FontWeight.w600 : FontWeight.w500, + color: isActive ? primary : onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/laundry_mobile/lib/staf/request_pickup.dart b/laundry_mobile/lib/staf/request_pickup.dart new file mode 100644 index 0000000..5237509 --- /dev/null +++ b/laundry_mobile/lib/staf/request_pickup.dart @@ -0,0 +1,287 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '../config.dart'; +import 'laundry/map.dart'; +import 'package:latlong2/latlong.dart'; + +class RequestPickupPage extends StatefulWidget { + final Map userData; + const RequestPickupPage({super.key, required this.userData}); + + @override + State createState() => _RequestPickupPageState(); +} + +class _RequestPickupPageState extends State { + final _formKey = GlobalKey(); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _phoneController = TextEditingController(); + final TextEditingController _addressController = TextEditingController(); + + double? _latitude; + double? _longitude; + bool _isLoading = false; + + // --- Tailwind DESIGN TOKENS --- + final Color primary = const Color(0xFF0058BE); + final Color onPrimary = const Color(0xFFFFFFFF); + final Color background = const Color(0xFFF7F9FB); + final Color surfaceContainerLowest = const Color(0xFFFFFFFF); + final Color onSurface = const Color(0xFF191C1E); + final Color onSurfaceVariant = const Color(0xFF424754); + final Color outlineVariant = const Color(0xFFC2C6D6); + final Color successGreen = const Color(0xFF10B981); + + void _showSnackBar(String msg, Color bg) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Icon( + bg == const Color(0xFF10B981) + ? Icons.check_circle_outline + : Icons.error_outline_rounded, + color: Colors.white, + size: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + bg == const Color(0xFF10B981) ? "Berhasil" : "Peringatan", + style: GoogleFonts.inter( + fontWeight: FontWeight.w800, + fontSize: 14, + color: Colors.white), + ), + Text( + msg, + style: GoogleFonts.inter( + fontSize: 12, + color: Colors.white.withOpacity(0.9), + fontWeight: FontWeight.w500), + ), + ], + ), + ), + ], + ), + ), + behavior: SnackBarBehavior.floating, + backgroundColor: bg.withOpacity(0.95), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: Colors.white.withOpacity(0.2), width: 1), + ), + duration: const Duration(seconds: 3), + )); + } + + Future _submitPickup() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _isLoading = true); + + try { + final response = await http.post( + Uri.parse("${AppConfig.baseUrl}/pickups"), + headers: { + 'Authorization': 'Bearer ${widget.userData['access_token']}', + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: json.encode({ + 'customer_name': _nameController.text.trim(), + 'phone': _phoneController.text.trim(), + 'address': _addressController.text.trim(), + 'latitude': _latitude, + 'longitude': _longitude, + }), + ); + + if (response.statusCode == 201) { + if (!mounted) return; + _showSnackBar( + "Jadwal jemput berhasil ditambahkan!", const Color(0xFF10B981)); + Navigator.pop(context, true); // Return true indicating success + } else { + if (!mounted) return; + _showSnackBar("Gagal menambahkan jadwal jemput", Colors.redAccent); + } + } catch (e) { + if (!mounted) return; + _showSnackBar("Terjadi kesalahan koneksi", Colors.redAccent); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + void _pickLocation() async { + LatLng? initial = _latitude != null && _longitude != null + ? LatLng(_latitude!, _longitude!) + : null; + + final selectedLoc = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MapPickerPage(initialLocation: initial))); + + if (selectedLoc != null && selectedLoc is LatLng) { + setState(() { + _latitude = selectedLoc.latitude; + _longitude = selectedLoc.longitude; + // Optionally update address field text if you implemented reverse geocoding, + // but here we just store the coords. + }); + _showSnackBar("Lokasi Peta berhasil dipilih!", const Color(0xFF10B981)); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: background, + appBar: AppBar( + backgroundColor: surfaceContainerLowest, + elevation: 0, + iconTheme: IconThemeData(color: onSurface), + title: Text("Jadwalkan Jemput", + style: GoogleFonts.inter( + color: onSurface, fontWeight: FontWeight.w700, fontSize: 18)), + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Informasi Pelanggan", + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w700, + color: onSurface)), + const SizedBox(height: 16), + _buildTextField( + "Nama Pelanggan", Icons.person_rounded, _nameController), + const SizedBox(height: 16), + _buildTextField("Nomor WhatsApp", Icons.phone_android_rounded, + _phoneController, + isNumber: true), + const SizedBox(height: 24), + + Text("Alamat Penjemputan", + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w700, + color: onSurface)), + const SizedBox(height: 16), + _buildTextField( + "Detail Alamat", Icons.home_rounded, _addressController, + maxLines: 3), + const SizedBox(height: 12), + + // Map Picker Button + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _pickLocation, + icon: Icon(Icons.pin_drop_rounded, + color: _latitude != null + ? successGreen + : primary), + label: Text( + _latitude != null + ? "Lokasi Maps Tersimpan" + : "Tandai Lokasi Penjemputan", + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + color: _latitude != null + ? successGreen + : primary)), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + side: BorderSide( + color: _latitude != null + ? successGreen + : outlineVariant, + width: 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + ), + ), + + const SizedBox(height: 40), + + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + elevation: 0, + ), + onPressed: _isLoading ? null : _submitPickup, + child: _isLoading + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: onPrimary, strokeWidth: 2.5)) + : Text("Kirim Tugas ke Driver", + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w800, + color: onPrimary)), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildTextField( + String label, IconData icon, TextEditingController controller, + {bool isNumber = false, int maxLines = 1}) { + return TextFormField( + controller: controller, + keyboardType: isNumber ? TextInputType.phone : TextInputType.text, + maxLines: maxLines, + style: GoogleFonts.inter( + fontSize: 14, fontWeight: FontWeight.w600, color: onSurface), + decoration: InputDecoration( + labelText: label, + labelStyle: GoogleFonts.inter(color: onSurfaceVariant, fontSize: 13), + prefixIcon: + maxLines == 1 ? Icon(icon, color: onSurfaceVariant, size: 20) : null, + filled: true, + fillColor: surfaceContainerLowest, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: outlineVariant)), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: outlineVariant.withOpacity(0.5))), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: primary)), + ), + validator: (val) => + (val == null || val.isEmpty) ? "Harap isi bidang ini" : null, + ); + } +} diff --git a/laundry_mobile/linux/.gitignore b/laundry_mobile/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/laundry_mobile/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/laundry_mobile/linux/CMakeLists.txt b/laundry_mobile/linux/CMakeLists.txt new file mode 100644 index 0000000..a312193 --- /dev/null +++ b/laundry_mobile/linux/CMakeLists.txt @@ -0,0 +1,145 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "laundry_mobile") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.laundry_mobile") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/laundry_mobile/linux/flutter/CMakeLists.txt b/laundry_mobile/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/laundry_mobile/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/laundry_mobile/linux/flutter/generated_plugin_registrant.cc b/laundry_mobile/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..f6f23bf --- /dev/null +++ b/laundry_mobile/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/laundry_mobile/linux/flutter/generated_plugin_registrant.h b/laundry_mobile/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/laundry_mobile/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/laundry_mobile/linux/flutter/generated_plugins.cmake b/laundry_mobile/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..f16b4c3 --- /dev/null +++ b/laundry_mobile/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/laundry_mobile/linux/main.cc b/laundry_mobile/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/laundry_mobile/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/laundry_mobile/linux/my_application.cc b/laundry_mobile/linux/my_application.cc new file mode 100644 index 0000000..c478e40 --- /dev/null +++ b/laundry_mobile/linux/my_application.cc @@ -0,0 +1,124 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "laundry_mobile"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "laundry_mobile"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/laundry_mobile/linux/my_application.h b/laundry_mobile/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/laundry_mobile/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/laundry_mobile/macos/.gitignore b/laundry_mobile/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/laundry_mobile/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/laundry_mobile/macos/Flutter/Flutter-Debug.xcconfig b/laundry_mobile/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/laundry_mobile/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/laundry_mobile/macos/Flutter/Flutter-Release.xcconfig b/laundry_mobile/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/laundry_mobile/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/laundry_mobile/macos/Flutter/GeneratedPluginRegistrant.swift b/laundry_mobile/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..fb0420b --- /dev/null +++ b/laundry_mobile/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,24 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import firebase_core +import firebase_database +import geolocator_apple +import mobile_scanner +import path_provider_foundation +import shared_preferences_foundation +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin")) + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/laundry_mobile/macos/Runner.xcodeproj/project.pbxproj b/laundry_mobile/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3e64445 --- /dev/null +++ b/laundry_mobile/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* laundry_mobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "laundry_mobile.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* laundry_mobile.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* laundry_mobile.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.laundryMobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/laundry_mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/laundry_mobile"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.laundryMobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/laundry_mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/laundry_mobile"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.laundryMobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/laundry_mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/laundry_mobile"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/laundry_mobile/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/laundry_mobile/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/laundry_mobile/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/laundry_mobile/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/laundry_mobile/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..33456c1 --- /dev/null +++ b/laundry_mobile/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/laundry_mobile/macos/Runner.xcworkspace/contents.xcworkspacedata b/laundry_mobile/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/laundry_mobile/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/laundry_mobile/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/laundry_mobile/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/laundry_mobile/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/laundry_mobile/macos/Runner/AppDelegate.swift b/laundry_mobile/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..8e02df2 --- /dev/null +++ b/laundry_mobile/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/laundry_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/laundry_mobile/macos/Runner/Base.lproj/MainMenu.xib b/laundry_mobile/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/laundry_mobile/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/laundry_mobile/macos/Runner/Configs/AppInfo.xcconfig b/laundry_mobile/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..b6434dd --- /dev/null +++ b/laundry_mobile/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = laundry_mobile + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.laundryMobile + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright ยฉ 2026 com.example. All rights reserved. diff --git a/laundry_mobile/macos/Runner/Configs/Debug.xcconfig b/laundry_mobile/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/laundry_mobile/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/laundry_mobile/macos/Runner/Configs/Release.xcconfig b/laundry_mobile/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/laundry_mobile/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/laundry_mobile/macos/Runner/Configs/Warnings.xcconfig b/laundry_mobile/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/laundry_mobile/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/laundry_mobile/macos/Runner/DebugProfile.entitlements b/laundry_mobile/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/laundry_mobile/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/laundry_mobile/macos/Runner/Info.plist b/laundry_mobile/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/laundry_mobile/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/laundry_mobile/macos/Runner/MainFlutterWindow.swift b/laundry_mobile/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/laundry_mobile/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/laundry_mobile/macos/Runner/Release.entitlements b/laundry_mobile/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/laundry_mobile/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/laundry_mobile/macos/RunnerTests/RunnerTests.swift b/laundry_mobile/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/laundry_mobile/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/laundry_mobile/pubspec.lock b/laundry_mobile/pubspec.lock new file mode 100644 index 0000000..1d35ea3 --- /dev/null +++ b/laundry_mobile/pubspec.lock @@ -0,0 +1,778 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: bda3b7b55958bfd867addc40d067b4b11f7b8846d57671f5b5a6e7f9a56fe3ad + url: "https://pub.dev" + source: hosted + version: "1.3.69" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_earcut: + dependency: transitive + description: + name: dart_earcut + sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b + url: "https://pub.dev" + source: hosted + version: "1.2.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: d5a94b884dcb1e6d3430298e94bfe002238094cdfd5e29202d536ee2120f9158 + url: "https://pub.dev" + source: hosted + version: "4.7.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: dc5096257cd67292d34d78ceeb90836f02a4be921b5f3934311a02bb2376118c + url: "https://pub.dev" + source: hosted + version: "3.6.0" + firebase_database: + dependency: "direct main" + description: + name: firebase_database + sha256: "9b527722dfb154692f4e9f3a3c38dd6f6a49c1849b01d9b4fe1b9e420bb98030" + url: "https://pub.dev" + source: hosted + version: "12.3.0" + firebase_database_platform_interface: + dependency: transitive + description: + name: firebase_database_platform_interface + sha256: b39da15c2a7399aac55f437ebd48d9b325f8c6310d792d80d1812efd91d4a8cf + url: "https://pub.dev" + source: hosted + version: "0.3.1+1" + firebase_database_web: + dependency: transitive + description: + name: firebase_database_web + sha256: c592343817b0fef558715d752b11d054b55aef393c4b0b826659b106a1e4d79c + url: "https://pub.dev" + source: hosted + version: "0.2.7+6" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + sha256: "2ecb34619a4be19df6f40c2f8dce1591675b4eff7a6857bd8f533706977385da" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: f62bcd90459e63210bbf9c35deb6a51c521f992a78de19a1fe5c11704f9530e2 + url: "https://pub.dev" + source: hosted + version: "13.0.4" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + url: "https://pub.dev" + source: hosted + version: "4.6.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: df9763500dadba0155373e9cb44e202ce21bd9ed5de6bdbd05c5854e86839cb8 + url: "https://pub.dev" + source: hosted + version: "6.3.0" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + latlong2: + dependency: "direct main" + description: + name: latlong2 + sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" + url: "https://pub.dev" + source: hosted + version: "0.9.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + logger: + dependency: transitive + description: + name: logger + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173" + url: "https://pub.dev" + source: hosted + version: "6.0.11" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + url: "https://pub.dev" + source: hosted + version: "2.2.15" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + polylabel: + dependency: transitive + description: + name: polylabel + sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + proj4dart: + dependency: transitive + description: + name: proj4dart + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" + source: hosted + version: "2.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "9f9f3d372d4304723e6136663bb291c0b93f5e4c8a4a6314347f481a33bda2b1" + url: "https://pub.dev" + source: hosted + version: "2.4.7" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" + url: "https://pub.dev" + source: hosted + version: "6.3.14" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.5.4 <4.0.0" + flutter: ">=3.24.0" diff --git a/laundry_mobile/pubspec.yaml b/laundry_mobile/pubspec.yaml new file mode 100644 index 0000000..d9567a9 --- /dev/null +++ b/laundry_mobile/pubspec.yaml @@ -0,0 +1,105 @@ +name: laundry_mobile +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.5.4 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + google_fonts: ^6.1.0 + http: ^1.1.0 + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + firebase_core: ^4.5.0 + firebase_database: ^12.1.4 + shared_preferences: ^2.5.3 + flutter_map: ^7.0.2 + latlong2: ^0.9.1 + geolocator: ^13.0.1 + url_launcher: ^6.2.5 + mobile_scanner: ^6.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_launcher_icons: ^0.13.1 + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^4.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + assets: + - assets/ + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package + +flutter_launcher_icons: + android: "launcher_icon" + ios: true + image_path: "assets/app_icon.png" diff --git a/laundry_mobile/test/widget_test.dart b/laundry_mobile/test/widget_test.dart new file mode 100644 index 0000000..4f91cb5 --- /dev/null +++ b/laundry_mobile/test/widget_test.dart @@ -0,0 +1,31 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:laundry_mobile/main.dart'; +import 'package:laundry_mobile/login.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp(initialPage: LoginPage())); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/laundry_mobile/web/favicon.png b/laundry_mobile/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/laundry_mobile/web/favicon.png differ diff --git a/laundry_mobile/web/icons/Icon-192.png b/laundry_mobile/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/laundry_mobile/web/icons/Icon-192.png differ diff --git a/laundry_mobile/web/icons/Icon-512.png b/laundry_mobile/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/laundry_mobile/web/icons/Icon-512.png differ diff --git a/laundry_mobile/web/icons/Icon-maskable-192.png b/laundry_mobile/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/laundry_mobile/web/icons/Icon-maskable-192.png differ diff --git a/laundry_mobile/web/icons/Icon-maskable-512.png b/laundry_mobile/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/laundry_mobile/web/icons/Icon-maskable-512.png differ diff --git a/laundry_mobile/web/index.html b/laundry_mobile/web/index.html new file mode 100644 index 0000000..1559308 --- /dev/null +++ b/laundry_mobile/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + mylaundry + + + + + + diff --git a/laundry_mobile/web/manifest.json b/laundry_mobile/web/manifest.json new file mode 100644 index 0000000..3ec05c7 --- /dev/null +++ b/laundry_mobile/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "MyLaundry", + "short_name": "MyLaundry", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} \ No newline at end of file diff --git a/laundry_mobile/windows/.gitignore b/laundry_mobile/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/laundry_mobile/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/laundry_mobile/windows/CMakeLists.txt b/laundry_mobile/windows/CMakeLists.txt new file mode 100644 index 0000000..82f07f2 --- /dev/null +++ b/laundry_mobile/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(laundry_mobile LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "laundry_mobile") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/laundry_mobile/windows/flutter/CMakeLists.txt b/laundry_mobile/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/laundry_mobile/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/laundry_mobile/windows/flutter/generated_plugin_registrant.cc b/laundry_mobile/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..d461042 --- /dev/null +++ b/laundry_mobile/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + FirebaseDatabasePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseDatabasePluginCApi")); + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/laundry_mobile/windows/flutter/generated_plugin_registrant.h b/laundry_mobile/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/laundry_mobile/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/laundry_mobile/windows/flutter/generated_plugins.cmake b/laundry_mobile/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..0d5fd8e --- /dev/null +++ b/laundry_mobile/windows/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + firebase_core + firebase_database + geolocator_windows + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/laundry_mobile/windows/runner/CMakeLists.txt b/laundry_mobile/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/laundry_mobile/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/laundry_mobile/windows/runner/Runner.rc b/laundry_mobile/windows/runner/Runner.rc new file mode 100644 index 0000000..cfd1ef4 --- /dev/null +++ b/laundry_mobile/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "laundry_mobile" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "laundry_mobile" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "laundry_mobile.exe" "\0" + VALUE "ProductName", "laundry_mobile" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/laundry_mobile/windows/runner/flutter_window.cpp b/laundry_mobile/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/laundry_mobile/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/laundry_mobile/windows/runner/flutter_window.h b/laundry_mobile/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/laundry_mobile/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/laundry_mobile/windows/runner/main.cpp b/laundry_mobile/windows/runner/main.cpp new file mode 100644 index 0000000..cec8457 --- /dev/null +++ b/laundry_mobile/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"laundry_mobile", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/laundry_mobile/windows/runner/resource.h b/laundry_mobile/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/laundry_mobile/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/laundry_mobile/windows/runner/resources/app_icon.ico b/laundry_mobile/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/laundry_mobile/windows/runner/resources/app_icon.ico differ diff --git a/laundry_mobile/windows/runner/runner.exe.manifest b/laundry_mobile/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/laundry_mobile/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/laundry_mobile/windows/runner/utils.cpp b/laundry_mobile/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/laundry_mobile/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/laundry_mobile/windows/runner/utils.h b/laundry_mobile/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/laundry_mobile/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/laundry_mobile/windows/runner/win32_window.cpp b/laundry_mobile/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/laundry_mobile/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/laundry_mobile/windows/runner/win32_window.h b/laundry_mobile/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/laundry_mobile/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/mylaundry.ino b/mylaundry.ino new file mode 100644 index 0000000..6bf1791 --- /dev/null +++ b/mylaundry.ino @@ -0,0 +1,543 @@ +#include "HX711.h" +#include +#include +#include +#include +#include +#include +#include + +#include "addons/RTDBHelper.h" +#include "addons/TokenHelper.h" + +// KONFIGURASI FIREBASE +#define API_KEY "AIzaSyDtb-5Qs13elSodm8gRJ-rGj0kornwd1BY" +#define DATABASE_URL "mylaundry-d02fb-default-rtdb.asia-southeast1.firebasedatabase.app" + +// PIN HARDWARE +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +#define OLED_RESET -1 +#define I2C_ADDRESS 0x3C +#define LOADCELL_DOUT_PIN 32 +#define LOADCELL_SCK_PIN 33 +#define BUTTON_PIN 27 +#define BUZZER_PIN 25 + +// OBJEK GLOBAL +Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); +HX711 scale; + +float calibration_factor = 103500.00; + +Preferences preferences; +FirebaseData fbdo; +FirebaseData streamDo; +FirebaseAuth auth; +FirebaseConfig config; + +// BERAT REALTIME & FITUR LOCK +float currentWeight = 0.000; +bool isLocked = false; +float lockedWeight = 0.000; +float lastWeight = 0.000; +unsigned long stableStartTime = 0; + +// PENGATURAN SENSITIVITAS LOCK +const float TOLERANSI_STABIL = 0.005f; +const float TOLERANSI_UNLOCK = 0.02f; +const unsigned long WAKTU_STABIL = 2000; + +// SERIAL MONITOR +unsigned long lastSerialTime = 0; +#define SERIAL_PRINT_INTERVAL 500 + +// STATE DISPLAY +String strJasa = "-"; +String strEst = "-"; +String strTipeLayanan = "-"; +String strBerat = "0.000"; +String strHarga = "Rp0"; + +// FIREBASE PATH +String firebasePath = "/laundry"; + +// TEXT +int scrollPosX_Jasa = 42; +int scrollPosX_Berat = 42; +unsigned long lastScrollTime = 0; + +// STREAM HEALTH CHECK +unsigned long lastStreamCheck = 0; +#define STREAM_CHECK_INTERVAL 30000 + +void beepBuzzer(int times) { + for (int i = 0; i < times; i++) { + digitalWrite(BUZZER_PIN, HIGH); + delay(150); + digitalWrite(BUZZER_PIN, LOW); + delay(100); + } +} + +String getValue(String data, char separator, int index) { + int found = 0; + int strIndex[] = {0, -1}; + int maxIndex = data.length() - 1; + for (int i = 0; i <= maxIndex && found <= index; i++) { + if (data.charAt(i) == separator || i == maxIndex) { + found++; + strIndex[0] = strIndex[1] + 1; + strIndex[1] = (i == maxIndex) ? i + 1 : i; + } + } + return found > index ? data.substring(strIndex[0], strIndex[1]) : ""; +} + +void printDisplayState() { + Serial.println("-----------------------------"); + Serial.println("[DISPLAY STATE]"); + Serial.print(" Jasa : "); Serial.println(strJasa); + Serial.print(" Est : "); Serial.println(strEst); + Serial.print(" Tipe : "); Serial.println(strTipeLayanan); + Serial.print(" Berat : "); Serial.println(strBerat); + Serial.print(" Harga : "); Serial.println(strHarga); + Serial.println("-----------------------------"); +} + +void reconnectStream() { + Serial.println("[STREAM] Menyambung ulang stream Firebase..."); + Firebase.RTDB.endStream(&streamDo); + delay(200); + + if (Firebase.RTDB.getString(&fbdo, firebasePath + "/status_hp")) { + String rawData = fbdo.stringData(); + Serial.print("[STREAM] Data status_hp saat reconnect: "); + Serial.println(rawData); + strJasa = getValue(rawData, '|', 0); strJasa.trim(); + strEst = getValue(rawData, '|', 1); strEst.trim(); + strTipeLayanan = getValue(rawData, '|', 2); strTipeLayanan.trim(); + strBerat = getValue(rawData, '|', 3); strBerat.trim(); + strHarga = getValue(rawData, '|', 4); strHarga.trim(); + printDisplayState(); + } else { + Serial.println("[STREAM] Gagal baca status_hp saat reconnect"); + } + + if (Firebase.RTDB.beginMultiPathStream(&streamDo, firebasePath)) { + Serial.println("[STREAM] Stream berhasil disambung ulang"); + } else { + Serial.println("[STREAM] GAGAL menyambung ulang stream!"); + } +} + +void setup() { + Serial.begin(9600); + Serial.println("\n============================="); + Serial.println("[BOOT] Sistem mulai..."); + Wire.begin(21, 22); + + pinMode(BUTTON_PIN, INPUT_PULLUP); + pinMode(BUZZER_PIN, OUTPUT); + digitalWrite(BUZZER_PIN, LOW); + + display.begin(I2C_ADDRESS, true); + display.setTextWrap(false); + display.clearDisplay(); + display.setTextColor(SH110X_WHITE); + display.setTextSize(1); + + // CEK RESET WIFI + if (digitalRead(BUTTON_PIN) == LOW) { + Serial.println("[BOOT] Tombol ditekan saat boot โ€” mode reset WiFi"); + display.clearDisplay(); + display.setCursor(10, 30); + display.print("Tahan 5 detik = Reset"); + display.display(); + + unsigned long pressStart = millis(); + while (digitalRead(BUTTON_PIN) == LOW && (millis() - pressStart < 5000)) { + delay(10); + } + + if (millis() - pressStart >= 5000) { + Serial.println("[BOOT] WiFi direset!"); + display.clearDisplay(); + display.setCursor(10, 30); + display.print("WiFi Direset!"); + display.setCursor(10, 45); + display.print("Lepas Tombol..."); + display.display(); + beepBuzzer(3); + + WiFiManager wm; + wm.resetSettings(); + delay(1000); + + while (digitalRead(BUTTON_PIN) == LOW) { delay(10); } + delay(500); + ESP.restart(); + } + } + +// WIFI MANAGER + Serial.println("[WIFI] Menghubungkan ke WiFi..."); + display.clearDisplay(); + display.setCursor(0, 14); display.print("Menghubungkan WiFi"); + display.setCursor(0, 28); display.print("Jika gagal,"); + display.setCursor(0, 42); display.print("buka: laundry.net"); + display.display(); + + WiFiManager wm; + wm.setCaptivePortalEnable(true); + wm.setHostname("laundry"); + + wm.setConnectTimeout(15); + wm.setConfigPortalTimeout(60); + bool connected = wm.autoConnect("MyLaundry"); + + if (!connected) { + Serial.println("[WIFI] GAGAL terhubung! Restart..."); + display.clearDisplay(); + display.setCursor(10, 30); display.print("WiFi Gagal!"); + display.setCursor(10, 42); display.print("Restart..."); + display.display(); + delay(3000); + ESP.restart(); + } + + Serial.print("[WIFI] Terhubung! IP: "); + Serial.println(WiFi.localIP()); + + // FIREBASE + Serial.println("[FIREBASE] Inisialisasi Firebase..."); + config.api_key = API_KEY; + config.database_url = DATABASE_URL; + config.signer.test_mode = true; + config.token_status_callback = tokenStatusCallback; + fbdo.setBSSLBufferSize(4096, 1024); + Firebase.begin(&config, &auth); + Firebase.reconnectWiFi(true); + Serial.println("[FIREBASE] Firebase siap"); + + // LOAD CELL + Serial.println("[LOADCELL] Inisialisasi load cell..."); + scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN); + scale.set_scale(calibration_factor); + + display.clearDisplay(); + display.setCursor(10, 35); display.print("Tunggu 5 detik"); + display.display(); + Serial.println("[LOADCELL] Warm-up 5 detik..."); + delay(5000); + + scale.tare(); + Serial.println("[LOADCELL] Tare selesai, siap menimbang"); + + // CEK STATUS + Serial.println("[FIREBASE] Membaca status_hp awal..."); + if (Firebase.RTDB.getString(&fbdo, firebasePath + "/status_hp")) { + String rawData = fbdo.stringData(); + Serial.print("[FIREBASE] status_hp awal: "); + Serial.println(rawData); + strJasa = getValue(rawData, '|', 0); strJasa.trim(); + strEst = getValue(rawData, '|', 1); strEst.trim(); + strTipeLayanan = getValue(rawData, '|', 2); strTipeLayanan.trim(); + strBerat = getValue(rawData, '|', 3); strBerat.trim(); + strHarga = getValue(rawData, '|', 4); strHarga.trim(); + printDisplayState(); + } else { + Serial.println("[FIREBASE] Gagal baca status_hp awal"); + } + + if (Firebase.RTDB.beginMultiPathStream(&streamDo, firebasePath)) { + Serial.println("[STREAM] Stream Firebase aktif"); + } else { + Serial.println("[STREAM] GAGAL memulai stream Firebase!"); + } + + display.clearDisplay(); + display.setCursor(10, 35); + display.print("Sistem Siap!"); + display.display(); + beepBuzzer(2); + delay(1000); + + lastStreamCheck = millis(); + Serial.println("[BOOT] Setup selesai, masuk loop"); + Serial.println("=============================\n"); +} + +void loop() { + + // CEK STREAM + if (millis() - lastStreamCheck > STREAM_CHECK_INTERVAL) { + lastStreamCheck = millis(); + + if (WiFi.status() != WL_CONNECTED) { + Serial.println("[WIFI] Koneksi terputus! Mencoba reconnect..."); + WiFi.reconnect(); + delay(2000); + if (WiFi.status() == WL_CONNECTED) { + Serial.print("[WIFI] Reconnect berhasil! IP: "); + Serial.println(WiFi.localIP()); + } else { + Serial.println("[WIFI] Reconnect GAGAL"); + } + } else { + Serial.println("[HEALTH] WiFi OK | Stream check..."); + } + + if (!streamDo.httpConnected()) { + Serial.println("[STREAM] Stream terputus, reconnect..."); + reconnectStream(); + } else { + Serial.println("[HEALTH] Stream OK"); + } + } + + // BACA STREAM FIREBASE + if (Firebase.ready() && Firebase.RTDB.readStream(&streamDo)) { + if (streamDo.streamAvailable()) { + String dPath = streamDo.dataPath(); + String dType = streamDo.dataType(); + + Serial.print("[STREAM] Event masuk โ€” path: "); + Serial.print(dPath); + Serial.print(" | type: "); + Serial.println(dType); + + if (dPath == "/status_hp" && dType == "string") { + String rawData = streamDo.stringData(); + Serial.print("[STREAM] status_hp baru: "); + Serial.println(rawData); + + String newJasa = getValue(rawData, '|', 0); newJasa.trim(); + if (newJasa != strJasa) { + Serial.print("[DISPLAY] Jasa berubah: "); Serial.print(strJasa); + Serial.print(" โ†’ "); Serial.println(newJasa); + strJasa = newJasa; + scrollPosX_Jasa = 42; + } + + strEst = getValue(rawData, '|', 1); strEst.trim(); + strTipeLayanan = getValue(rawData, '|', 2); strTipeLayanan.trim(); + + String newBerat = getValue(rawData, '|', 3); newBerat.trim(); + if (newBerat != strBerat) { + Serial.print("[DISPLAY] Berat berubah: "); Serial.print(strBerat); + Serial.print(" โ†’ "); Serial.println(newBerat); + strBerat = newBerat; + scrollPosX_Berat = 42; + } + + strHarga = getValue(rawData, '|', 4); strHarga.trim(); + printDisplayState(); + + } else if (dPath == "/screen_status" && dType == "string") { + String screenCmd = streamDo.stringData(); + Serial.print("[STREAM] screen_status: "); + Serial.println(screenCmd); + + if (screenCmd == "RESTART") { + Serial.println("[CMD] RESTART diterima โ€” reset nilai & reconnect stream"); + + strJasa = "-"; + strEst = "-"; + strTipeLayanan = "-"; + strBerat = "0.000"; + strHarga = "Rp0"; + scrollPosX_Jasa = 42; + scrollPosX_Berat = 42; + + Firebase.RTDB.setStringAsync(&fbdo, firebasePath + "/screen_status", "ON"); + Serial.println("[CMD] screen_status di-set ke ON"); + + reconnectStream(); + } else { + Serial.print("[CMD] screen_status '"); + Serial.print(screenCmd); + Serial.println("' diterima tapi tidak ada aksi"); + } + + } else if (dPath == "/current_weight" && dType == "float") { + Serial.print("[STREAM] current_weight update dari Firebase: "); + Serial.println(streamDo.floatData(), 3); + + } else { + Serial.print("[STREAM] Path tidak dihandle: "); + Serial.println(dPath); + } + } + } + + // LOAD CELL & LOGIKA AUTO-LOCK + if (scale.is_ready()) { + float raw_weight = scale.get_units(2); + + // Cegah nilai minus + if (raw_weight < 0.005f) { + raw_weight = 0.00f; + } + + if (isLocked) { + // Jika beban diangkat atau ditambah drastis, LEPASKAN KUNCI + if (abs(raw_weight - lockedWeight) > TOLERANSI_UNLOCK) { + isLocked = false; + Serial.println("[LOCK] Kunci terlepas!"); + } else { + currentWeight = lockedWeight; + } + } + + if (!isLocked) { + currentWeight = raw_weight; + + // Cek apakah berat mulai stabil (perubahan di bawah toleransi 20 gram) + if (abs(raw_weight - lastWeight) <= TOLERANSI_STABIL) { + + // Jika sudah stabil selama 2 detik, dan berat di atas 0.1 kg (jangan kunci timbangan kosong) + if (millis() - stableStartTime > WAKTU_STABIL && raw_weight > 0.1f) { + isLocked = true; + lockedWeight = raw_weight; + + Serial.print("[LOCK] Berat dikunci di: "); + Serial.println(lockedWeight, 3); + } + } else { + // Jika berat masih naik-turun, reset timer kestabilan + stableStartTime = millis(); + lastWeight = raw_weight; + } + } + } + // PANTAUAN SERIAL MONITOR + if (millis() - lastSerialTime > SERIAL_PRINT_INTERVAL) { + lastSerialTime = millis(); + Serial.print("Berat : "); + Serial.print(currentWeight, 3); + Serial.println(" kg"); + } + + // TICK SCROLL + bool tickScroll = false; + if (millis() - lastScrollTime > 30) { + tickScroll = true; + lastScrollTime = millis(); + } + + display.clearDisplay(); + display.setTextSize(1); + + //TEXT JASA + int textWidthJasa = strJasa.length() * 6; + if (textWidthJasa > (128 - 42)) { + if (tickScroll) { + scrollPosX_Jasa--; + if (scrollPosX_Jasa < -textWidthJasa) scrollPosX_Jasa = 128; + } + display.setCursor(scrollPosX_Jasa, 10); + display.print(strJasa); + display.fillRect(0, 10, 42, 9, SH110X_BLACK); + } else { + scrollPosX_Jasa = 42; + display.setCursor(42, 10); + display.print(strJasa); + } + display.setCursor(0, 10); + display.print("Jasa : "); + + // ESTIMASI + display.setCursor(0, 21); + display.print("Est : "); + display.print(strEst); + + // TOTAL RINCIAN + String fullBerat = "-"; + if (strJasa != "-" && strJasa != "") { + if (strBerat != "" && strBerat != "0" && strBerat != "0.0" && strBerat != "0.000") { + fullBerat = strBerat; + } + } + + int textWidthBerat = fullBerat.length() * 6; + if (textWidthBerat > (128 - 42)) { + if (tickScroll) { + scrollPosX_Berat--; + if (scrollPosX_Berat < -textWidthBerat) scrollPosX_Berat = 128; + } + display.setCursor(scrollPosX_Berat, 32); + display.print(fullBerat); + display.fillRect(0, 32, 42, 9, SH110X_BLACK); + } else { + scrollPosX_Berat = 42; + display.setCursor(42, 32); + display.print(fullBerat); + } + display.setCursor(0, 32); + display.print("Total: "); + + // HARGA + display.setCursor(0, 43); + display.print("Harga: "); + display.print(strHarga); + + // GARIS & STATUS BAR + display.drawLine(0, 52, 128, 52, SH110X_WHITE); + display.setCursor(0, 55); + + display.print("Berat: "); + display.print(currentWeight, 3); + display.print(" Kg"); + + if (currentWeight >= 20.0f) { + display.print(" OVL!"); + } + + // TOMBOL KIRIM BERAT + if (digitalRead(BUTTON_PIN) == LOW) { + delay(50); + if (digitalRead(BUTTON_PIN) == LOW) { + Serial.print("[BUTTON] Tombol ditekan โ€” kirim berat: "); + Serial.print(currentWeight, 3); + Serial.println(" kg"); + + display.fillRect(0, 54, 128, 10, SH110X_BLACK); + display.setCursor(0, 55); + display.print("Mengirim..."); + display.display(); + + bool berhasil = false; + if (Firebase.ready()) { + unsigned long startSend = millis(); + if (Firebase.RTDB.setFloat(&fbdo, firebasePath + "/current_weight", currentWeight)) { + if (millis() - startSend < 5000) { + berhasil = true; + } + } + } + + if (berhasil) { + Serial.println("[BUTTON] Kirim berat BERHASIL"); + beepBuzzer(2); + } else { + Serial.println("[BUTTON] Kirim berat GAGAL โ€” reconnect stream"); + display.fillRect(0, 54, 128, 10, SH110X_BLACK); + display.setCursor(0, 55); + display.print("GAGAL! Coba lagi"); + display.display(); + beepBuzzer(1); + delay(1500); + reconnectStream(); + } + + unsigned long startWait = millis(); + while (digitalRead(BUTTON_PIN) == LOW && (millis() - startWait < 2000)) { + delay(10); + } + } + } + + display.display(); + delay(10); +}