Add Projek TA
This commit is contained in:
parent
3d9a39fda6
commit
7a7f610f83
|
@ -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
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
|
@ -0,0 +1,70 @@
|
|||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
MIDTRANS_CLIENT_KEY=SB-Mid-client-R_h8dtZUuy1pK8fn
|
||||
MIDTRANS_SERVER_KEY=SB-Mid-server-yOklCwe5I_63lC5lDN3k8Kyw
|
||||
MIDTRANS_IS_PRODUCTION=false
|
||||
MIDTRANS_MERCHANT_ID=G937265596
|
|
@ -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
|
|
@ -0,0 +1,23 @@
|
|||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/auth.json
|
||||
/.fleet
|
||||
/.idea
|
||||
/.nova
|
||||
/.vscode
|
||||
/.zed
|
|
@ -0,0 +1,14 @@
|
|||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
# Mengizinkan akses langsung ke storage
|
||||
RewriteCond %{REQUEST_URI} ^/storage/(.*)$
|
||||
RewriteRule ^storage/(.*)$ public/storage/$1 [L,NC]
|
||||
|
||||
# Redirect semua request ke public folder
|
||||
RewriteRule ^(.*)$ public/$1 [L]
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
Header set Access-Control-Allow-Origin "*"
|
||||
</IfModule>
|
|
@ -0,0 +1,172 @@
|
|||
# Dokumentasi API Verifikasi Email
|
||||
|
||||
## Daftar Isi
|
||||
|
||||
1. [Pengenalan](#pengenalan)
|
||||
2. [Endpoint API Verifikasi Email](#endpoint-api-verifikasi-email)
|
||||
3. [Pengujian di Postman](#pengujian-di-postman)
|
||||
4. [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Pengenalan
|
||||
|
||||
API Verifikasi Email digunakan untuk memverifikasi alamat email pengguna setelah registrasi. Sistem menggunakan link verifikasi yang dikirim ke email pengguna. Email verifikasi memiliki tampilan yang profesional dan berisi link yang dapat diklik untuk memverifikasi akun.
|
||||
|
||||
## Endpoint API Verifikasi Email
|
||||
|
||||
### 1. Kirim Email Verifikasi
|
||||
|
||||
- **URL**: `/api/email/verification-notification`
|
||||
- **Method**: `POST`
|
||||
- **Headers**:
|
||||
```
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
- **Response Sukses** (200):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"verification_url": "http://example.com/verify-email?expires=1234&id=1&signature=abcdef"
|
||||
},
|
||||
"message": "Link verifikasi email telah dikirim."
|
||||
}
|
||||
```
|
||||
- **Response Error** (400):
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Email sudah terverifikasi."
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Verifikasi Email
|
||||
|
||||
- **URL**: `/api/email/verify/{id}/{hash}`
|
||||
- **Method**: `GET`
|
||||
- **Headers**:
|
||||
```
|
||||
Accept: application/json
|
||||
```
|
||||
- **Response Sukses** (200):
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Email berhasil diverifikasi.",
|
||||
"redirect_url": "http://example.com/login"
|
||||
}
|
||||
```
|
||||
- **Response Error** (400):
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Invalid/Expired url provided.",
|
||||
"redirect_url": "http://example.com/email-verification-error"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Kirim Ulang Email Verifikasi
|
||||
|
||||
- **URL**: `/api/email/verification-resend`
|
||||
- **Method**: `POST`
|
||||
- **Headers**:
|
||||
```
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
- **Response Sukses** (200):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"verification_url": "http://example.com/verify-email?expires=1234&id=1&signature=abcdef"
|
||||
},
|
||||
"message": "Link verifikasi email telah dikirim ulang."
|
||||
}
|
||||
```
|
||||
- **Response Error** (400):
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Email sudah terverifikasi."
|
||||
}
|
||||
```
|
||||
|
||||
## Pengujian di Postman
|
||||
|
||||
### Persiapan Collection
|
||||
|
||||
1. Buka Postman dan buat collection baru bernama "Project Jahit API"
|
||||
2. Tambahkan environment variable:
|
||||
- `base_url`: `http://localhost:8000/api`
|
||||
- `token`: (kosong, akan diisi setelah login)
|
||||
|
||||
### Langkah-langkah Pengujian
|
||||
|
||||
#### 1. Login untuk Mendapatkan Token
|
||||
|
||||
1. **Request**:
|
||||
- Method: `POST`
|
||||
- URL: `{{base_url}}/pelanggan/login` atau `{{base_url}}/penjahit/login`
|
||||
- Body (raw JSON):
|
||||
```json
|
||||
{
|
||||
"email": "email@example.com",
|
||||
"password": "password"
|
||||
}
|
||||
```
|
||||
|
||||
2. **Simpan Token**:
|
||||
- Dari response, salin nilai token
|
||||
- Simpan di environment variable `token`
|
||||
|
||||
#### 2. Kirim Email Verifikasi
|
||||
|
||||
1. **Request**:
|
||||
- Method: `POST`
|
||||
- URL: `{{base_url}}/email/verification-notification`
|
||||
- Headers:
|
||||
```
|
||||
Authorization: Bearer {{token}}
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
2. **Cek Email**:
|
||||
- Buka email yang terdaftar
|
||||
- Anda akan menerima email dengan tampilan profesional
|
||||
- Email berisi tombol "Verifikasi Email Saya"
|
||||
|
||||
#### 3. Verifikasi Email
|
||||
|
||||
1. **Klik Link Verifikasi**:
|
||||
- Klik tombol "Verifikasi Email Saya" di email
|
||||
- Atau salin URL dari email dan buka di browser
|
||||
|
||||
2. **Hasil**:
|
||||
- Jika berhasil, Anda akan diarahkan ke halaman login
|
||||
- Status akun Anda akan berubah menjadi terverifikasi
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Masalah Umum:
|
||||
|
||||
1. **"Email sudah terverifikasi"**
|
||||
- Akun Anda sudah terverifikasi sebelumnya
|
||||
- Tidak perlu melakukan verifikasi lagi
|
||||
|
||||
2. **"Invalid/Expired url provided"**
|
||||
- Link verifikasi sudah kadaluarsa (lebih dari 60 menit)
|
||||
- Gunakan endpoint `/api/email/verification-resend` untuk mendapatkan link baru
|
||||
|
||||
3. **"User not found"**
|
||||
- ID pengguna dalam link tidak valid
|
||||
- Pastikan menggunakan link terbaru
|
||||
|
||||
### Tips:
|
||||
|
||||
- Link verifikasi hanya berlaku selama 60 menit
|
||||
- Jika link kadaluarsa, gunakan fitur "Kirim Ulang Email Verifikasi"
|
||||
- Pastikan Anda login sebelum meminta link verifikasi baru
|
||||
- Periksa folder spam jika email tidak muncul di inbox
|
|
@ -0,0 +1,106 @@
|
|||
# Project Jahit API
|
||||
|
||||
REST API untuk platform Project Jahit yang menghubungkan pelanggan dengan penjahit profesional.
|
||||
|
||||
## Tentang Proyek
|
||||
|
||||
Project Jahit adalah aplikasi yang memungkinkan pelanggan mencari penjahit terdekat, melakukan pemesanan jasa, dan melacak status pesanan. Penjahit dapat mengelola toko, layanan, galeri, dan booking.
|
||||
|
||||
## Teknologi
|
||||
|
||||
- Laravel 10
|
||||
- MySQL
|
||||
- Laravel Sanctum (Autentikasi)
|
||||
|
||||
## Fitur Utama
|
||||
|
||||
- Registrasi dan login untuk pelanggan, penjahit, dan admin
|
||||
- Pencarian penjahit berdasarkan spesialisasi
|
||||
- Pemesanan jasa jahit
|
||||
- Manajemen booking
|
||||
- Pembayaran (mock)
|
||||
- Rating dan ulasan
|
||||
- Galeri hasil jahitan
|
||||
- Manajemen layanan
|
||||
- Dashboard untuk semua role
|
||||
|
||||
## Instalasi
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/username/project-jahit-api.git
|
||||
cd project-jahit-api
|
||||
|
||||
# Install dependencies
|
||||
composer install
|
||||
|
||||
# Salin file .env.example ke .env
|
||||
cp .env.example .env
|
||||
|
||||
# Generate application key
|
||||
php artisan key:generate
|
||||
|
||||
# Setup database di file .env
|
||||
# DB_DATABASE=project_jahit
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
# Migrasi dan seed database
|
||||
php artisan migrate --seed
|
||||
|
||||
# Buat symlink untuk storage
|
||||
php artisan storage:link
|
||||
|
||||
# Jalankan server development
|
||||
php artisan serve
|
||||
```
|
||||
|
||||
## Endpoints API
|
||||
|
||||
API menyediakan berbagai endpoint untuk semua fitur. Dokumentasi lengkap dapat diakses melalui:
|
||||
|
||||
```
|
||||
http://localhost:8000/docs
|
||||
```
|
||||
|
||||
atau dengan membuka file `documentation.md` untuk format Markdown.
|
||||
|
||||
## Struktur Folder
|
||||
|
||||
- `app/Http/Controllers/Api` - API controllers
|
||||
- `app/Models` - Model data aplikasi
|
||||
- `database/migrations` - Migrasi database
|
||||
- `routes/api.php` - Definisi route API
|
||||
- `storage/app/public` - Storage untuk file yang diupload
|
||||
- `resources/views/api-docs.blade.php` - Halaman dokumentasi API
|
||||
|
||||
## Role Pengguna
|
||||
|
||||
Aplikasi memiliki 3 role dengan hak akses berbeda:
|
||||
|
||||
1. **Admin** - Mengelola seluruh data
|
||||
2. **Penjahit** - Mengelola profil toko, layanan, dan booking
|
||||
3. **Pelanggan** - Mencari penjahit, membuat booking, dan memberikan rating
|
||||
|
||||
## Pengujian
|
||||
|
||||
```bash
|
||||
# Menjalankan unit test
|
||||
php artisan test
|
||||
```
|
||||
|
||||
## Akses File Media
|
||||
|
||||
File yang diupload dapat diakses melalui URL:
|
||||
|
||||
```
|
||||
http://localhost:8000/storage/{path}
|
||||
```
|
||||
|
||||
Contoh:
|
||||
|
||||
```
|
||||
http://localhost:8000/storage/design_photos/file.jpg
|
||||
http://localhost:8000/storage/gallery_photos/photo.jpg
|
||||
http://localhost:8000/storage/profile_photos/avatar.jpg
|
||||
```
|
|
@ -0,0 +1,207 @@
|
|||
# Dokumentasi Reset Password API
|
||||
|
||||
## Daftar Isi
|
||||
|
||||
1. [Pengenalan](#pengenalan)
|
||||
2. [Endpoint Reset Password](#endpoint-reset-password)
|
||||
3. [Pengujian di Postman](#pengujian-di-postman)
|
||||
4. [Tampilan Email](#tampilan-email)
|
||||
5. [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Pengenalan
|
||||
|
||||
API Reset Password memungkinkan pengguna untuk mereset password mereka jika lupa. Sistem menggunakan PIN 6 digit yang dikirim ke email pengguna. Email reset password memiliki tampilan yang profesional dan berisi PIN yang dapat digunakan untuk mereset password.
|
||||
|
||||
## Endpoint Reset Password
|
||||
|
||||
### 1. Request Reset Password (Kirim PIN)
|
||||
|
||||
- **URL**:
|
||||
- Untuk pelanggan: `/api/pelanggan/forgot-password`
|
||||
- Untuk penjahit: `/api/penjahit/forgot-password`
|
||||
- **Method**: `POST`
|
||||
- **Headers**:
|
||||
```
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
```
|
||||
- **Body**:
|
||||
```json
|
||||
{
|
||||
"email": "email@example.com"
|
||||
}
|
||||
```
|
||||
- **Response Sukses** (200):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"message": "PIN reset password telah dikirim ke email Anda"
|
||||
},
|
||||
"message": "Kami sudah mengirim PIN reset password ke email Anda"
|
||||
}
|
||||
```
|
||||
- **Response Error** (404):
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Error.",
|
||||
"data": {
|
||||
"email": "Email tidak ditemukan"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Reset Password (Gunakan PIN)
|
||||
|
||||
- **URL**:
|
||||
- Untuk pelanggan: `/api/pelanggan/reset-password`
|
||||
- Untuk penjahit: `/api/penjahit/reset-password`
|
||||
- **Method**: `POST`
|
||||
- **Headers**:
|
||||
```
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
```
|
||||
- **Body**:
|
||||
```json
|
||||
{
|
||||
"email": "email@example.com",
|
||||
"password": "password_baru",
|
||||
"password_confirmation": "password_baru",
|
||||
"pin": "123456"
|
||||
}
|
||||
```
|
||||
- **Response Sukses** (200):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [],
|
||||
"message": "Password berhasil direset"
|
||||
}
|
||||
```
|
||||
- **Response Error** (400):
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Error.",
|
||||
"data": {
|
||||
"pin": "PIN tidak valid atau sudah kadaluarsa"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pengujian di Postman
|
||||
|
||||
### Persiapan Collection
|
||||
|
||||
1. Buka Postman dan buat collection baru bernama "Project Jahit API"
|
||||
2. Tambahkan environment variable:
|
||||
- `base_url`: `http://localhost:8000/api`
|
||||
- `email`: `email@example.com` (ganti dengan email yang terdaftar)
|
||||
|
||||
### Langkah-langkah Pengujian
|
||||
|
||||
#### 1. Meminta PIN Reset Password
|
||||
|
||||
1. **Buat Request**:
|
||||
- Method: `POST`
|
||||
- URL: `{{base_url}}/pelanggan/forgot-password` (untuk pelanggan) atau `{{base_url}}/penjahit/forgot-password` (untuk penjahit)
|
||||
- Headers:
|
||||
```
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
```
|
||||
- Body (raw JSON):
|
||||
```json
|
||||
{
|
||||
"email": "{{email}}"
|
||||
}
|
||||
```
|
||||
|
||||
2. **Kirim Request**:
|
||||
- Klik tombol "Send"
|
||||
- Pastikan mendapat response sukses (200)
|
||||
- Pesan sukses: "PIN reset password telah dikirim ke email Anda"
|
||||
|
||||
3. **Cek Email**:
|
||||
- Buka email yang terdaftar
|
||||
- Anda akan menerima email dengan tampilan profesional
|
||||
- Email berisi PIN 6 digit untuk reset password
|
||||
- Catat PIN tersebut untuk langkah berikutnya
|
||||
|
||||
#### 2. Reset Password dengan PIN
|
||||
|
||||
1. **Buat Request**:
|
||||
- Method: `POST`
|
||||
- URL: `{{base_url}}/pelanggan/reset-password` (untuk pelanggan) atau `{{base_url}}/penjahit/reset-password` (untuk penjahit)
|
||||
- Headers:
|
||||
```
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
```
|
||||
- Body (raw JSON):
|
||||
```json
|
||||
{
|
||||
"email": "{{email}}",
|
||||
"password": "password_baru",
|
||||
"password_confirmation": "password_baru",
|
||||
"pin": "123456" // Ganti dengan PIN yang diterima di email
|
||||
}
|
||||
```
|
||||
|
||||
2. **Kirim Request**:
|
||||
- Klik tombol "Send"
|
||||
- Pastikan mendapat response sukses (200)
|
||||
- Pesan sukses: "Password berhasil direset"
|
||||
|
||||
3. **Verifikasi Password Baru**:
|
||||
- Coba login dengan password baru
|
||||
- Pastikan bisa masuk ke sistem
|
||||
|
||||
## Tampilan Email
|
||||
|
||||
Email reset password memiliki tampilan profesional dengan:
|
||||
|
||||
- Logo aplikasi di header
|
||||
- Judul "Reset Password"
|
||||
- Salam personal dengan nama pengguna
|
||||
- PIN reset password dalam kotak yang menonjol
|
||||
- Informasi keamanan dan masa berlaku PIN
|
||||
- Footer dengan informasi kontak dan copyright
|
||||
|
||||
Email dirancang untuk mudah dibaca di berbagai perangkat dan klien email.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error Umum:
|
||||
|
||||
1. **"Email tidak ditemukan"**
|
||||
- Pastikan menggunakan email yang sudah terdaftar
|
||||
- Pastikan menggunakan endpoint yang sesuai (pelanggan/penjahit)
|
||||
- Cek penulisan email
|
||||
|
||||
2. **"PIN tidak valid atau sudah kadaluarsa"**
|
||||
- Pastikan menggunakan PIN terbaru dari email
|
||||
- PIN hanya berlaku selama 60 menit
|
||||
- PIN harus tepat 6 digit
|
||||
|
||||
3. **"Error validasi"**
|
||||
- Password minimal 8 karakter
|
||||
- Password dan konfirmasi harus sama
|
||||
- Email harus valid
|
||||
|
||||
### Tips:
|
||||
|
||||
- Selalu gunakan PIN terbaru dari email
|
||||
- Jika PIN expired, ulangi proses dari awal
|
||||
- Pastikan semua field terisi dengan benar
|
||||
- Periksa folder spam jika email tidak muncul di inbox
|
||||
- PIN hanya dapat digunakan satu kali
|
||||
|
||||
### Kontak Support:
|
||||
|
||||
Jika mengalami masalah, hubungi:
|
||||
|
||||
- Email: support@projectjahit.com
|
||||
- Telepon: (021) xxx-xxxx
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,396 @@
|
|||
<?php
|
||||
|
||||
// @formatter:off
|
||||
// phpcs:ignoreFile
|
||||
/**
|
||||
* A helper file for your Eloquent Models
|
||||
* Copy the phpDocs from this file to the correct Model,
|
||||
* And remove them from this file, to prevent double declarations.
|
||||
*
|
||||
* @author Barry vd. Heuvel <barryvdh@gmail.com>
|
||||
*/
|
||||
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
* @property string $bank_name
|
||||
* @property string $account_number
|
||||
* @property string $account_holder_name
|
||||
* @property string $status
|
||||
* @property string|null $rejection_reason
|
||||
* @property \Illuminate\Support\Carbon|null $verified_at
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\User $user
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Withdrawal> $withdrawals
|
||||
* @property-read int|null $withdrawals_count
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|BankAccount newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|BankAccount newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|BankAccount query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|BankAccount whereAccountHolderName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|BankAccount whereAccountNumber($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|BankAccount whereBankName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|BankAccount whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|BankAccount whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|BankAccount whereRejectionReason($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|BankAccount whereStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|BankAccount whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|BankAccount whereUserId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|BankAccount whereVerifiedAt($value)
|
||||
*/
|
||||
class BankAccount extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $transaction_code
|
||||
* @property int $customer_id
|
||||
* @property int $tailor_id
|
||||
* @property \Illuminate\Support\Carbon $appointment_date
|
||||
* @property \Illuminate\Support\Carbon $appointment_time
|
||||
* @property string $service_type
|
||||
* @property string $category
|
||||
* @property string|null $design_photo
|
||||
* @property string|null $notes
|
||||
* @property string $status
|
||||
* @property numeric|null $total_price
|
||||
* @property string $payment_status
|
||||
* @property \Illuminate\Support\Carbon|null $completion_date
|
||||
* @property array<array-key, mixed>|null $measurements
|
||||
* @property array<array-key, mixed>|null $repair_details
|
||||
* @property string|null $repair_photo
|
||||
* @property string|null $repair_notes
|
||||
* @property string|null $completion_photo
|
||||
* @property string|null $completion_notes
|
||||
* @property \Illuminate\Support\Carbon|null $accepted_at
|
||||
* @property \Illuminate\Support\Carbon|null $rejected_at
|
||||
* @property \Illuminate\Support\Carbon|null $completed_at
|
||||
* @property \Illuminate\Support\Carbon|null $pickup_date
|
||||
* @property string|null $rejection_reason
|
||||
* @property string|null $payment_method
|
||||
* @property string|null $midtrans_snap_token
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\User $customer
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\TailorRating> $ratings
|
||||
* @property-read int|null $ratings_count
|
||||
* @property-read \App\Models\User $tailor
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereAcceptedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereAppointmentDate($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereAppointmentTime($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereCategory($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereCompletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereCompletionDate($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereCompletionNotes($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereCompletionPhoto($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereCustomerId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereDesignPhoto($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereMeasurements($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereMidtransSnapToken($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereNotes($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking wherePaymentMethod($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking wherePaymentStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking wherePickupDate($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereRejectedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereRejectionReason($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereRepairDetails($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereRepairNotes($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereRepairPhoto($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereServiceType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereTailorId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereTotalPrice($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereTransactionCode($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Booking whereUpdatedAt($value)
|
||||
*/
|
||||
class Booking extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
* @property string $photo
|
||||
* @property string|null $title
|
||||
* @property string|null $description
|
||||
* @property string|null $category
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\User $tailor
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorGallery newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorGallery newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorGallery query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorGallery whereCategory($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorGallery whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorGallery whereDescription($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorGallery whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorGallery wherePhoto($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorGallery whereTitle($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorGallery whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorGallery whereUserId($value)
|
||||
*/
|
||||
class TailorGallery extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $booking_id
|
||||
* @property int $customer_id
|
||||
* @property int $tailor_id
|
||||
* @property numeric $rating
|
||||
* @property string|null $review
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Booking $booking
|
||||
* @property-read \App\Models\User $customer
|
||||
* @property-read \App\Models\User $tailor
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorRating newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorRating newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorRating query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorRating whereBookingId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorRating whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorRating whereCustomerId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorRating whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorRating whereRating($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorRating whereReview($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorRating whereTailorId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorRating whereUpdatedAt($value)
|
||||
*/
|
||||
class TailorRating extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
* @property string $name
|
||||
* @property string $description
|
||||
* @property numeric $price
|
||||
* @property string $category
|
||||
* @property int $estimated_days
|
||||
* @property bool $is_available
|
||||
* @property string|null $service_photo
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\User $tailor
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorService newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorService newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorService query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorService whereCategory($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorService whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorService whereDescription($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorService whereEstimatedDays($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorService whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorService whereIsAvailable($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorService whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorService wherePrice($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorService whereServicePhoto($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorService whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorService whereUserId($value)
|
||||
*/
|
||||
class TailorService extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $category
|
||||
* @property string|null $icon
|
||||
* @property string|null $photo
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $tailors
|
||||
* @property-read int|null $tailors_count
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorSpecialization newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorSpecialization newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorSpecialization query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorSpecialization whereCategory($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorSpecialization whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorSpecialization whereIcon($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorSpecialization whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorSpecialization whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorSpecialization wherePhoto($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TailorSpecialization whereUpdatedAt($value)
|
||||
*/
|
||||
class TailorSpecialization extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property string $password
|
||||
* @property string $role
|
||||
* @property string|null $phone_number
|
||||
* @property string|null $address
|
||||
* @property float|null $latitude
|
||||
* @property float|null $longitude
|
||||
* @property string|null $shop_description
|
||||
* @property string|null $profile_photo
|
||||
* @property \Illuminate\Support\Carbon|null $email_verified_at
|
||||
* @property string|null $remember_token
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\BankAccount> $bankAccounts
|
||||
* @property-read int|null $bank_accounts_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Booking> $bookings
|
||||
* @property-read int|null $bookings_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Booking> $customerBookings
|
||||
* @property-read int|null $customer_bookings_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\TailorGallery> $gallery
|
||||
* @property-read int|null $gallery_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\TailorRating> $givenRatings
|
||||
* @property-read int|null $given_ratings_count
|
||||
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
|
||||
* @property-read int|null $notifications_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\TailorSpecialization> $preferredSpecializations
|
||||
* @property-read int|null $preferred_specializations_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\TailorRating> $ratings
|
||||
* @property-read int|null $ratings_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\TailorService> $services
|
||||
* @property-read int|null $services_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\TailorSpecialization> $specializations
|
||||
* @property-read int|null $specializations_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Laravel\Sanctum\PersonalAccessToken> $tokens
|
||||
* @property-read int|null $tokens_count
|
||||
* @property-read \App\Models\Wallet|null $wallet
|
||||
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereAddress($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmail($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmailVerifiedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereLatitude($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereLongitude($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePassword($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePhoneNumber($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereProfilePhoto($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRememberToken($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRole($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereShopDescription($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereUpdatedAt($value)
|
||||
*/
|
||||
class User extends \Eloquent implements \Illuminate\Contracts\Auth\MustVerifyEmail {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
* @property float $balance
|
||||
* @property string $status
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\WalletTransaction> $transactions
|
||||
* @property-read int|null $transactions_count
|
||||
* @property-read \App\Models\User $user
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Withdrawal> $withdrawals
|
||||
* @property-read int|null $withdrawals_count
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Wallet newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Wallet newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Wallet query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Wallet whereBalance($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Wallet whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Wallet whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Wallet whereStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Wallet whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Wallet whereUserId($value)
|
||||
*/
|
||||
class Wallet extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $wallet_id
|
||||
* @property int|null $booking_id
|
||||
* @property string $type
|
||||
* @property float $amount
|
||||
* @property string $description
|
||||
* @property string $status
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Booking|null $booking
|
||||
* @property-read \App\Models\Wallet $wallet
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|WalletTransaction newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|WalletTransaction newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|WalletTransaction query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|WalletTransaction whereAmount($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|WalletTransaction whereBookingId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|WalletTransaction whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|WalletTransaction whereDescription($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|WalletTransaction whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|WalletTransaction whereStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|WalletTransaction whereType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|WalletTransaction whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|WalletTransaction whereWalletId($value)
|
||||
*/
|
||||
class WalletTransaction extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $wallet_id
|
||||
* @property int $bank_account_id
|
||||
* @property float $amount
|
||||
* @property string $status
|
||||
* @property string|null $rejection_reason
|
||||
* @property string|null $proof_of_payment
|
||||
* @property \Illuminate\Support\Carbon|null $processed_at
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\BankAccount $bankAccount
|
||||
* @property-read \App\Models\Wallet $wallet
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Withdrawal newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Withdrawal newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Withdrawal query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Withdrawal whereAmount($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Withdrawal whereBankAccountId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Withdrawal whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Withdrawal whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Withdrawal whereProcessedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Withdrawal whereProofOfPayment($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Withdrawal whereRejectionReason($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Withdrawal whereStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Withdrawal whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Withdrawal whereWalletId($value)
|
||||
*/
|
||||
class Withdrawal extends \Eloquent {}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\Booking;
|
||||
use App\Models\TailorRating;
|
||||
use App\Models\TailorService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class AdminDashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
// Get counts
|
||||
$totalPenjahit = User::where('role', 'penjahit')->count();
|
||||
$totalPelanggan = User::where('role', 'pelanggan')->count();
|
||||
$totalBookings = Booking::count();
|
||||
$totalServices = TailorService::count();
|
||||
|
||||
// Get average rating for all tailors
|
||||
$averageRating = TailorRating::avg('rating') ?? 0;
|
||||
|
||||
// Get bookings statistics
|
||||
$bookingStats = Booking::select('status', DB::raw('count(*) as total'))
|
||||
->groupBy('status')
|
||||
->get()
|
||||
->pluck('total', 'status')
|
||||
->toArray();
|
||||
|
||||
// Get recent bookings
|
||||
$recentBookings = Booking::with(['customer:id,name', 'tailor:id,name'])
|
||||
->latest()
|
||||
->take(5)
|
||||
->get()
|
||||
->map(function ($booking) {
|
||||
return [
|
||||
'id' => $booking->id,
|
||||
'customer_name' => $booking->customer->name,
|
||||
'tailor_name' => $booking->tailor->name,
|
||||
'status' => $booking->status,
|
||||
'created_at' => $booking->created_at
|
||||
];
|
||||
});
|
||||
|
||||
// Get monthly booking statistics for the last 6 months
|
||||
$monthlyStats = Booking::select(
|
||||
DB::raw('DATE_FORMAT(created_at, "%Y-%m") as month'),
|
||||
DB::raw('count(*) as total_bookings'),
|
||||
DB::raw('SUM(CASE WHEN status = "completed" THEN 1 ELSE 0 END) as completed_bookings')
|
||||
)
|
||||
->where('created_at', '>=', Carbon::now()->subMonths(6))
|
||||
->groupBy('month')
|
||||
->orderBy('month')
|
||||
->get();
|
||||
|
||||
// Get top rated tailors
|
||||
$topTailors = User::where('role', 'penjahit')
|
||||
->withAvg('ratings as average_rating', 'rating')
|
||||
->withCount('ratings')
|
||||
->withCount('bookings')
|
||||
->having('ratings_count', '>', 0)
|
||||
->orderByDesc('average_rating')
|
||||
->take(5)
|
||||
->get()
|
||||
->map(function ($tailor) {
|
||||
return [
|
||||
'id' => $tailor->id,
|
||||
'name' => $tailor->name,
|
||||
'average_rating' => round($tailor->average_rating, 1),
|
||||
'total_ratings' => $tailor->ratings_count,
|
||||
'total_bookings' => $tailor->bookings_count
|
||||
];
|
||||
});
|
||||
|
||||
// Get latest registered users (both tailors and customers)
|
||||
$recentUsers = User::whereIn('role', ['penjahit', 'pelanggan'])
|
||||
->latest()
|
||||
->take(5)
|
||||
->select('id', 'name', 'role', 'created_at')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'summary' => [
|
||||
'total_penjahit' => $totalPenjahit,
|
||||
'total_pelanggan' => $totalPelanggan,
|
||||
'total_bookings' => $totalBookings,
|
||||
'total_services' => $totalServices,
|
||||
'average_rating' => round($averageRating, 1)
|
||||
],
|
||||
'booking_statistics' => [
|
||||
'by_status' => $bookingStats,
|
||||
'monthly' => $monthlyStats
|
||||
],
|
||||
'recent_bookings' => $recentBookings,
|
||||
'top_tailors' => $topTailors,
|
||||
'recent_users' => $recentUsers
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Failed to retrieve dashboard data',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,220 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Withdrawal;
|
||||
use App\Models\BankAccount;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class AdminWithdrawalController extends BaseController
|
||||
{
|
||||
public function getPendingWithdrawals()
|
||||
{
|
||||
try {
|
||||
$withdrawals = Withdrawal::with(['wallet.user', 'bankAccount'])
|
||||
->where('status', 'pending')
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
return $this->sendResponse($withdrawals, 'Pending withdrawals retrieved successfully');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function getPendingBankAccounts()
|
||||
{
|
||||
try {
|
||||
$pendingBankAccounts = BankAccount::with('user')
|
||||
->where('status', 'pending')
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
return $this->sendResponse($pendingBankAccounts, 'Pending bank accounts retrieved successfully');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function processWithdrawal(Request $request, Withdrawal $withdrawal)
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'status' => 'required|in:completed,rejected',
|
||||
'rejection_reason' => 'required_if:status,rejected|string|nullable',
|
||||
'proof_of_payment' => 'required_if:status,completed|file|image|max:2048'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Validation Error.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
// Pastikan status masih pending
|
||||
if ($withdrawal->status !== 'pending') {
|
||||
return $this->sendError('Error.', ['status' => ['Withdrawal sudah diproses sebelumnya']], 422);
|
||||
}
|
||||
|
||||
if ($request->status === 'rejected') {
|
||||
// Kembalikan saldo ke wallet
|
||||
$withdrawal->wallet->addBalance(
|
||||
$withdrawal->amount,
|
||||
'Refund for rejected withdrawal #' . $withdrawal->id
|
||||
);
|
||||
|
||||
$withdrawal->update([
|
||||
'status' => 'rejected',
|
||||
'rejection_reason' => $request->rejection_reason,
|
||||
'processed_at' => now()
|
||||
]);
|
||||
|
||||
return $this->sendResponse($withdrawal, 'Withdrawal rejected successfully');
|
||||
} else {
|
||||
// Status completed
|
||||
if ($request->hasFile('proof_of_payment')) {
|
||||
$path = $request->file('proof_of_payment')->store('proof_of_payments', 'public');
|
||||
|
||||
$withdrawal->update([
|
||||
'status' => 'completed',
|
||||
'rejection_reason' => null,
|
||||
'proof_of_payment' => $path,
|
||||
'processed_at' => now()
|
||||
]);
|
||||
|
||||
return $this->sendResponse($withdrawal, 'Withdrawal completed successfully');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendError('Error.', ['proof_of_payment' => ['Bukti pembayaran wajib diupload untuk status completed']], 422);
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function verifyBankAccount(Request $request, BankAccount $bankAccount)
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'status' => 'required|in:active,rejected',
|
||||
'rejection_reason' => 'required_if:status,rejected|string|nullable'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Validation Error.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'status' => $request->status,
|
||||
'verified_at' => now()
|
||||
];
|
||||
|
||||
// Hanya tambahkan rejection_reason jika status rejected
|
||||
if ($request->status === 'rejected') {
|
||||
$data['rejection_reason'] = $request->rejection_reason;
|
||||
} else {
|
||||
$data['rejection_reason'] = null; // Set null ketika status active
|
||||
}
|
||||
|
||||
$bankAccount->update($data);
|
||||
|
||||
return $this->sendResponse($bankAccount, 'Bank account verification completed');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function getVerifiedBankAccounts(Request $request)
|
||||
{
|
||||
try {
|
||||
$query = BankAccount::with('user')
|
||||
->whereIn('status', ['active', 'rejected'])
|
||||
->where('verified_at', '!=', null);
|
||||
|
||||
// Filter berdasarkan status jika parameter status disediakan
|
||||
if ($request->has('status') && in_array($request->status, ['active', 'rejected'])) {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
// Filter berdasarkan pencarian nama pengguna atau nomor rekening
|
||||
if ($request->has('search')) {
|
||||
$search = $request->search;
|
||||
$query->whereHas('user', function($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%");
|
||||
})->orWhere('account_number', 'like', "%{$search}%")
|
||||
->orWhere('account_holder_name', 'like', "%{$search}%")
|
||||
->orWhere('bank_name', 'like', "%{$search}%");
|
||||
}
|
||||
|
||||
$verifiedBankAccounts = $query->latest('verified_at')->get();
|
||||
|
||||
return $this->sendResponse($verifiedBankAccounts, 'Verified bank accounts retrieved successfully');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function getWithdrawalHistory(Request $request)
|
||||
{
|
||||
try {
|
||||
$query = Withdrawal::with(['wallet.user', 'bankAccount'])
|
||||
->whereIn('status', ['completed', 'rejected']);
|
||||
|
||||
// Filter berdasarkan status
|
||||
if ($request->has('status') && in_array($request->status, ['completed', 'rejected'])) {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
// Filter berdasarkan pencarian nama pengguna, nomor rekening atau bank
|
||||
if ($request->has('search')) {
|
||||
$search = $request->search;
|
||||
$query->whereHas('wallet.user', function($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%");
|
||||
})->orWhereHas('bankAccount', function($q) use ($search) {
|
||||
$q->where('account_number', 'like', "%{$search}%")
|
||||
->orWhere('account_holder_name', 'like', "%{$search}%")
|
||||
->orWhere('bank_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Filter berdasarkan periode tanggal
|
||||
if ($request->has('from_date')) {
|
||||
$query->whereDate('created_at', '>=', $request->from_date);
|
||||
}
|
||||
|
||||
if ($request->has('to_date')) {
|
||||
$query->whereDate('created_at', '<=', $request->to_date);
|
||||
}
|
||||
|
||||
// Pengurutan
|
||||
$sortBy = $request->get('sort_by', 'processed_at');
|
||||
$sortOrder = $request->get('sort_order', 'desc');
|
||||
|
||||
if (in_array($sortBy, ['created_at', 'amount', 'processed_at'])) {
|
||||
$query->orderBy($sortBy, $sortOrder);
|
||||
}
|
||||
|
||||
// Paginasi hasil
|
||||
$perPage = $request->get('per_page', 15);
|
||||
$withdrawals = $query->paginate($perPage);
|
||||
|
||||
return $this->sendResponse($withdrawals, 'Withdrawal history retrieved successfully');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function getBankAccountDetail(BankAccount $bankAccount)
|
||||
{
|
||||
try {
|
||||
// Load user dan riwayat withdrawal
|
||||
$bankAccount->load(['user', 'withdrawals']);
|
||||
|
||||
return $this->sendResponse($bankAccount, 'Bank account detail retrieved successfully');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,479 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\TailorSpecialization;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class AuthController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Get specialization data with photos
|
||||
*/
|
||||
private function getSpecializationData()
|
||||
{
|
||||
$specializations = \App\Models\TailorSpecialization::all()
|
||||
->groupBy('category')
|
||||
->map(function ($items) {
|
||||
return $items->map(function ($item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'name' => $item->name,
|
||||
'photo' => $item->photo,
|
||||
'category' => $item->category
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
return $specializations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register pelanggan
|
||||
*/
|
||||
public function registerPelanggan(Request $request)
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:8',
|
||||
'phone_number' => 'required|string|max:15',
|
||||
'address' => 'required|string|max:500',
|
||||
'preferred_specializations' => 'nullable|array',
|
||||
'preferred_specializations.*' => 'exists:tailor_specializations,id',
|
||||
'latitude' => 'required|numeric|between:-90,90',
|
||||
'longitude' => 'required|numeric|between:-180,180'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'role' => 'pelanggan',
|
||||
'phone_number' => $request->phone_number,
|
||||
'address' => $request->address,
|
||||
'latitude' => $request->latitude,
|
||||
'longitude' => $request->longitude
|
||||
]);
|
||||
|
||||
if ($request->has('preferred_specializations') && !empty($request->preferred_specializations)) {
|
||||
$user->preferredSpecializations()->attach($request->preferred_specializations);
|
||||
}
|
||||
|
||||
// Send verification email
|
||||
$user->sendEmailVerificationNotification();
|
||||
|
||||
$token = $user->createToken('auth_token', ['pelanggan'])->plainTextToken;
|
||||
|
||||
// Get all specializations with photos
|
||||
$specializations = $this->getSpecializationData();
|
||||
|
||||
return $this->sendResponse([
|
||||
'user' => $user,
|
||||
'access_token' => $token,
|
||||
'token_type' => 'Bearer',
|
||||
'specializations' => $specializations,
|
||||
'selected_specializations' => $user->preferredSpecializations
|
||||
], 'Registrasi pelanggan berhasil. Silakan cek email Anda untuk verifikasi.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error registrasi.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register penjahit
|
||||
*/
|
||||
public function registerPenjahit(Request $request)
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:8',
|
||||
'phone_number' => 'required|string|max:15',
|
||||
'address' => 'required|string|max:500',
|
||||
'shop_description' => 'required|string|max:1000',
|
||||
'specializations' => 'required|array|min:1',
|
||||
'specializations.*' => 'exists:tailor_specializations,id',
|
||||
'latitude' => 'required|numeric|between:-90,90',
|
||||
'longitude' => 'required|numeric|between:-180,180'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'role' => 'penjahit',
|
||||
'phone_number' => $request->phone_number,
|
||||
'address' => $request->address,
|
||||
'latitude' => $request->latitude,
|
||||
'longitude' => $request->longitude,
|
||||
'shop_description' => $request->shop_description
|
||||
]);
|
||||
|
||||
// Attach specializations
|
||||
$user->specializations()->attach($request->specializations);
|
||||
|
||||
// Send verification email
|
||||
$user->sendEmailVerificationNotification();
|
||||
|
||||
$token = $user->createToken('auth_token', ['penjahit'])->plainTextToken;
|
||||
|
||||
return $this->sendResponse([
|
||||
'user' => $user,
|
||||
'access_token' => $token,
|
||||
'token_type' => 'Bearer'
|
||||
], 'Registrasi penjahit berhasil. Silakan cek email Anda untuk verifikasi.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat registrasi'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login pelanggan
|
||||
*/
|
||||
public function loginPelanggan(Request $request)
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'email' => 'required|email',
|
||||
'password' => 'required'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
if (!Auth::attempt($request->only('email', 'password'))) {
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Email atau password salah'], 401);
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
// Check if user is pelanggan
|
||||
if (!$user->isPelanggan()) {
|
||||
Auth::logout();
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Akun ini bukan akun pelanggan'], 401);
|
||||
}
|
||||
|
||||
// Check if email is verified
|
||||
if (!$user->hasVerifiedEmail()) {
|
||||
// Resend verification email
|
||||
$user->sendEmailVerificationNotification();
|
||||
Auth::logout();
|
||||
return $this->sendError('Unauthorized.', [
|
||||
'error' => 'Email belum diverifikasi. Kami telah mengirim ulang email verifikasi ke ' . $user->email
|
||||
], 401);
|
||||
}
|
||||
|
||||
$token = $user->createToken('auth_token', ['pelanggan'])->plainTextToken;
|
||||
|
||||
// Get all specializations with photos
|
||||
$specializations = $this->getSpecializationData();
|
||||
|
||||
return $this->sendResponse([
|
||||
'user' => $user,
|
||||
'access_token' => $token,
|
||||
'token_type' => 'Bearer',
|
||||
'specializations' => $specializations,
|
||||
'selected_specializations' => $user->preferredSpecializations
|
||||
], 'Login pelanggan berhasil.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error login.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login penjahit
|
||||
*/
|
||||
public function loginPenjahit(Request $request)
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|string'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
if (!Auth::attempt($request->only('email', 'password'))) {
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Email atau password salah'], 401);
|
||||
}
|
||||
|
||||
$user = User::where('email', $request->email)->firstOrFail();
|
||||
|
||||
if ($user->role !== 'penjahit') {
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Akun ini bukan akun penjahit'], 401);
|
||||
}
|
||||
|
||||
// Check if email is verified
|
||||
if (!$user->hasVerifiedEmail()) {
|
||||
// Resend verification email
|
||||
$user->sendEmailVerificationNotification();
|
||||
Auth::logout();
|
||||
return $this->sendError('Unauthorized.', [
|
||||
'error' => 'Email belum diverifikasi. Kami telah mengirim ulang email verifikasi ke ' . $user->email
|
||||
], 401);
|
||||
}
|
||||
|
||||
$token = $user->createToken('auth_token', ['penjahit'])->plainTextToken;
|
||||
|
||||
return $this->sendResponse([
|
||||
'user' => $user,
|
||||
'access_token' => $token,
|
||||
'token_type' => 'Bearer'
|
||||
], 'Login penjahit berhasil');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat login'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login admin
|
||||
*/
|
||||
public function loginAdmin(Request $request)
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'email' => 'required|email',
|
||||
'password' => 'required'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
if (!Auth::attempt($request->only('email', 'password'))) {
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Email atau password salah'], 401);
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
if (!$user->isAdmin()) {
|
||||
Auth::logout();
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Akun ini bukan akun admin'], 401);
|
||||
}
|
||||
|
||||
$token = $user->createToken('auth_token', ['admin'])->plainTextToken;
|
||||
|
||||
return $this->sendResponse([
|
||||
'user' => $user,
|
||||
'access_token' => $token,
|
||||
'token_type' => 'Bearer'
|
||||
], 'Login admin berhasil.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error login.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
try {
|
||||
auth()->user()->tokens()->delete();
|
||||
return $this->sendResponse([], 'Logout berhasil');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat logout'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile
|
||||
*/
|
||||
public function profile()
|
||||
{
|
||||
try {
|
||||
$user = Auth::user();
|
||||
if ($user->role === 'penjahit') {
|
||||
$user->load('specializations');
|
||||
}
|
||||
return $this->sendResponse($user, 'Data profil berhasil diambil');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil profil'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all specializations
|
||||
*/
|
||||
public function getSpecializations()
|
||||
{
|
||||
try {
|
||||
$specializations = \App\Models\TailorSpecialization::all();
|
||||
return $this->sendResponse($specializations, 'Data spesialisasi berhasil diambil');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil data spesialisasi'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload foto profil
|
||||
*/
|
||||
public function uploadProfilePhoto(Request $request)
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'profile_photo' => 'required|image|mimes:jpeg,png,jpg|max:2048'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
// Hapus foto profil lama jika ada
|
||||
if ($user->profile_photo) {
|
||||
$oldPhotoPath = str_replace('/storage/', '', $user->profile_photo);
|
||||
if (Storage::disk('public')->exists($oldPhotoPath)) {
|
||||
Storage::disk('public')->delete($oldPhotoPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Upload dan simpan foto baru
|
||||
$photo = $request->file('profile_photo');
|
||||
$fileName = time() . '_' . $user->id . '.' . $photo->getClientOriginalExtension();
|
||||
$photo->storeAs('profile_photos', $fileName, 'public');
|
||||
|
||||
// Update path foto di database
|
||||
$user->profile_photo = '/storage/profile_photos/' . $fileName;
|
||||
$user->save();
|
||||
|
||||
return $this->sendResponse([
|
||||
'profile_photo' => $user->profile_photo
|
||||
], 'Foto profil berhasil diupload.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error upload foto.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user profile
|
||||
*/
|
||||
public function updateProfile(Request $request)
|
||||
{
|
||||
try {
|
||||
$user = Auth::user();
|
||||
|
||||
// Different validation rules based on user role
|
||||
$validationRules = [
|
||||
'name' => 'string|max:255',
|
||||
'phone_number' => 'string|max:15',
|
||||
'address' => 'string|max:500',
|
||||
'current_password' => 'string',
|
||||
'new_password' => 'string|min:8|confirmed',
|
||||
];
|
||||
|
||||
// Add penjahit specific validation rules
|
||||
if ($user->isPenjahit()) {
|
||||
$validationRules['shop_description'] = 'string|max:1000';
|
||||
$validationRules['specializations'] = 'array';
|
||||
$validationRules['specializations.*'] = 'exists:tailor_specializations,id';
|
||||
}
|
||||
|
||||
// Add pelanggan specific validation rules
|
||||
if ($user->isPelanggan()) {
|
||||
$validationRules['preferred_specializations'] = 'array';
|
||||
$validationRules['preferred_specializations.*'] = 'exists:tailor_specializations,id';
|
||||
$validationRules['latitude'] = 'numeric|between:-90,90';
|
||||
$validationRules['longitude'] = 'numeric|between:-180,180';
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), $validationRules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
// Check if password update is requested
|
||||
if ($request->has('current_password') && $request->has('new_password')) {
|
||||
// Verify current password
|
||||
if (!Hash::check($request->current_password, $user->password)) {
|
||||
return $this->sendError('Error validasi.', ['current_password' => 'Password saat ini tidak sesuai'], 422);
|
||||
}
|
||||
|
||||
// Update to new password
|
||||
$user->password = Hash::make($request->new_password);
|
||||
}
|
||||
|
||||
// Update basic profile fields
|
||||
if ($request->has('name')) {
|
||||
$user->name = $request->name;
|
||||
}
|
||||
|
||||
if ($request->has('phone_number')) {
|
||||
$user->phone_number = $request->phone_number;
|
||||
}
|
||||
|
||||
if ($request->has('address')) {
|
||||
$user->address = $request->address;
|
||||
}
|
||||
|
||||
// Update penjahit specific fields
|
||||
if ($user->isPenjahit()) {
|
||||
if ($request->has('shop_description')) {
|
||||
$user->shop_description = $request->shop_description;
|
||||
}
|
||||
|
||||
// Update specializations if provided
|
||||
if ($request->has('specializations')) {
|
||||
$user->specializations()->sync($request->specializations);
|
||||
}
|
||||
}
|
||||
|
||||
// Update pelanggan specific fields
|
||||
if ($user->isPelanggan()) {
|
||||
if ($request->has('latitude')) {
|
||||
$user->latitude = $request->latitude;
|
||||
}
|
||||
|
||||
if ($request->has('longitude')) {
|
||||
$user->longitude = $request->longitude;
|
||||
}
|
||||
|
||||
// Update preferred specializations if provided
|
||||
if ($request->has('preferred_specializations')) {
|
||||
$user->preferredSpecializations()->sync($request->preferred_specializations);
|
||||
}
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
// Load relationships based on user role
|
||||
if ($user->isPenjahit()) {
|
||||
$user->load('specializations');
|
||||
} elseif ($user->isPelanggan()) {
|
||||
$user->load('preferredSpecializations');
|
||||
}
|
||||
|
||||
return $this->sendResponse($user, 'Profil berhasil diperbarui');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat memperbarui profil: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class BaseController extends Controller
|
||||
{
|
||||
/**
|
||||
* Success response method.
|
||||
*
|
||||
* @param mixed $result
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function sendResponse($result, $message = '', $code = 200)
|
||||
{
|
||||
$response = [
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
return response()->json($response, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Error response method.
|
||||
*
|
||||
* @param string $error
|
||||
* @param array $errorMessages
|
||||
* @param int $code
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function sendError($error, $errorMessages = [], $code = 404)
|
||||
{
|
||||
$response = [
|
||||
'success' => false,
|
||||
'message' => $error,
|
||||
];
|
||||
|
||||
if (!empty($errorMessages)) {
|
||||
$response['data'] = $errorMessages;
|
||||
}
|
||||
|
||||
return response()->json($response, $code);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,280 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CustomerController extends Controller
|
||||
{
|
||||
public function getAllCustomers()
|
||||
{
|
||||
try {
|
||||
$customers = User::where('role', 'pelanggan')
|
||||
->with(['customerBookings', 'preferredSpecializations', 'givenRatings'])
|
||||
->get()
|
||||
->map(function ($customer) {
|
||||
return [
|
||||
'id' => $customer->id,
|
||||
'name' => $customer->name,
|
||||
'email' => $customer->email,
|
||||
'phone_number' => $customer->phone_number,
|
||||
'address' => $customer->address,
|
||||
'profile_photo' => $customer->profile_photo,
|
||||
'location' => [
|
||||
'latitude' => $customer->latitude,
|
||||
'longitude' => $customer->longitude
|
||||
],
|
||||
'preferred_specializations' => $customer->preferredSpecializations->map(function ($spec) {
|
||||
return [
|
||||
'id' => $spec->id,
|
||||
'name' => $spec->name,
|
||||
'photo' => $spec->photo
|
||||
];
|
||||
}),
|
||||
'bookings' => [
|
||||
'total' => $customer->customerBookings->count(),
|
||||
'completed' => $customer->customerBookings->where('status', 'completed')->count(),
|
||||
'recent' => $customer->customerBookings->take(5)->map(function ($booking) {
|
||||
return [
|
||||
'id' => $booking->id,
|
||||
'tailor_name' => $booking->tailor->name ?? 'Unknown',
|
||||
'status' => $booking->status,
|
||||
'appointment_date' => $booking->appointment_date,
|
||||
'total_price' => $booking->total_price
|
||||
];
|
||||
})
|
||||
],
|
||||
'ratings_given' => [
|
||||
'total' => $customer->givenRatings->count(),
|
||||
'average_rating' => round($customer->givenRatings->avg('rating') ?? 0, 1),
|
||||
'recent' => $customer->givenRatings->take(5)->map(function ($rating) {
|
||||
return [
|
||||
'rating' => $rating->rating,
|
||||
'review' => $rating->review,
|
||||
'tailor_name' => $rating->tailor->name ?? 'Unknown',
|
||||
'created_at' => $rating->created_at
|
||||
];
|
||||
})
|
||||
],
|
||||
'created_at' => $customer->created_at,
|
||||
'updated_at' => $customer->updated_at
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $customers
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Failed to retrieve customers data',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function getCustomerDetail($id)
|
||||
{
|
||||
try {
|
||||
$customer = User::where('role', 'pelanggan')
|
||||
->with(['customerBookings.tailor', 'preferredSpecializations', 'givenRatings.tailor'])
|
||||
->findOrFail($id);
|
||||
|
||||
$data = [
|
||||
'id' => $customer->id,
|
||||
'name' => $customer->name,
|
||||
'email' => $customer->email,
|
||||
'phone_number' => $customer->phone_number,
|
||||
'address' => $customer->address,
|
||||
'profile_photo' => $customer->profile_photo,
|
||||
'location' => [
|
||||
'latitude' => $customer->latitude,
|
||||
'longitude' => $customer->longitude
|
||||
],
|
||||
'preferred_specializations' => $customer->preferredSpecializations->map(function ($spec) {
|
||||
return [
|
||||
'id' => $spec->id,
|
||||
'name' => $spec->name,
|
||||
'photo' => $spec->photo
|
||||
];
|
||||
}),
|
||||
'bookings' => [
|
||||
'total' => $customer->customerBookings->count(),
|
||||
'completed' => $customer->customerBookings->where('status', 'completed')->count(),
|
||||
'history' => $customer->customerBookings->map(function ($booking) {
|
||||
return [
|
||||
'id' => $booking->id,
|
||||
'tailor_name' => $booking->tailor->name ?? 'Unknown',
|
||||
'status' => $booking->status,
|
||||
'appointment_date' => $booking->appointment_date,
|
||||
'total_price' => $booking->total_price,
|
||||
'service_type' => $booking->service_type,
|
||||
'notes' => $booking->notes,
|
||||
'created_at' => $booking->created_at
|
||||
];
|
||||
})
|
||||
],
|
||||
'ratings_given' => [
|
||||
'total' => $customer->givenRatings->count(),
|
||||
'average_rating' => round($customer->givenRatings->avg('rating') ?? 0, 1),
|
||||
'history' => $customer->givenRatings->map(function ($rating) {
|
||||
return [
|
||||
'rating' => $rating->rating,
|
||||
'review' => $rating->review,
|
||||
'tailor_name' => $rating->tailor->name ?? 'Unknown',
|
||||
'created_at' => $rating->created_at
|
||||
];
|
||||
})
|
||||
],
|
||||
'created_at' => $customer->created_at,
|
||||
'updated_at' => $customer->updated_at
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $data
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Failed to retrieve customer detail',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function getCustomerTransactions($id)
|
||||
{
|
||||
try {
|
||||
$customer = User::where('role', 'pelanggan')
|
||||
->with(['customerBookings.tailor'])
|
||||
->findOrFail($id);
|
||||
|
||||
$transactions = $customer->customerBookings->map(function ($booking) {
|
||||
return [
|
||||
'booking_id' => $booking->id,
|
||||
'tailor_name' => $booking->tailor->name ?? 'Unknown',
|
||||
'status' => $booking->status,
|
||||
'appointment_date' => $booking->appointment_date,
|
||||
'service_type' => $booking->service_type,
|
||||
'total_price' => $booking->total_price,
|
||||
'payment_status' => $booking->payment_status,
|
||||
'notes' => $booking->notes,
|
||||
'created_at' => $booking->created_at
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'customer_id' => $customer->id,
|
||||
'customer_name' => $customer->name,
|
||||
'total_transactions' => $transactions->count(),
|
||||
'total_spent' => $transactions->where('payment_status', 'paid')->sum('total_price'),
|
||||
'transactions' => $transactions
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Gagal mengambil data transaksi pelanggan',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function updateCustomerSpecializations(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
// Validasi input
|
||||
$request->validate([
|
||||
'specialization_ids' => 'required|array',
|
||||
'specialization_ids.*' => 'exists:tailor_specializations,id'
|
||||
]);
|
||||
|
||||
// Cari pelanggan
|
||||
$customer = User::where('role', 'pelanggan')->find($id);
|
||||
|
||||
if (!$customer) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Pelanggan tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Sync specializations
|
||||
$customer->preferredSpecializations()->sync($request->specialization_ids);
|
||||
|
||||
// Load ulang relasi untuk memastikan data terbaru
|
||||
$customer->load('preferredSpecializations');
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Spesialisasi pelanggan berhasil diperbarui',
|
||||
'data' => [
|
||||
'customer_id' => $customer->id,
|
||||
'customer_name' => $customer->name,
|
||||
'specializations' => $customer->preferredSpecializations->map(function ($spec) {
|
||||
return [
|
||||
'id' => $spec->id,
|
||||
'name' => $spec->name,
|
||||
'icon' => $spec->icon
|
||||
];
|
||||
})
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Validasi gagal',
|
||||
'errors' => $e->errors()
|
||||
], 422);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Gagal memperbarui spesialisasi pelanggan',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteCustomer($id)
|
||||
{
|
||||
try {
|
||||
$customer = User::where('role', 'pelanggan')->find($id);
|
||||
|
||||
if (!$customer) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Pelanggan tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Hapus relasi spesialisasi terlebih dahulu
|
||||
$customer->preferredSpecializations()->detach();
|
||||
|
||||
// Hapus customer
|
||||
$customer->delete();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Pelanggan berhasil dihapus'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Gagal menghapus pelanggan',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class DocumentationController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$docs = [
|
||||
'wallet' => File::get(base_path('docs/wallet-api.md')),
|
||||
// Add other documentation sections here
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $docs,
|
||||
'message' => 'API documentation retrieved successfully'
|
||||
]);
|
||||
}
|
||||
|
||||
public function show($section)
|
||||
{
|
||||
$path = base_path("docs/{$section}-api.md");
|
||||
|
||||
if (!File::exists($path)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Documentation section not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$content = File::get($path);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'section' => $section,
|
||||
'content' => $content
|
||||
],
|
||||
'message' => 'API documentation section retrieved successfully'
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use App\Models\User;
|
||||
|
||||
class EmailVerificationController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Send verification email
|
||||
*/
|
||||
public function sendVerificationEmail(Request $request)
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return $this->sendResponse([], 'Email sudah terverifikasi.');
|
||||
}
|
||||
|
||||
// Generate verification URL
|
||||
$verificationUrl = $this->generateVerificationUrl($request->user());
|
||||
|
||||
// Send custom verification email
|
||||
$request->user()->notify(new \App\Notifications\VerifyEmailNotification($verificationUrl));
|
||||
|
||||
return $this->sendResponse([
|
||||
'verification_url' => $verificationUrl
|
||||
], 'Link verifikasi email telah dikirim.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify email
|
||||
*/
|
||||
public function verify(Request $request, $id)
|
||||
{
|
||||
if (!URL::hasValidSignature($request)) {
|
||||
return response()->view('emails.verification-error', [
|
||||
'message' => 'Link verifikasi tidak valid atau sudah kadaluarsa.'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$user = User::find($id);
|
||||
|
||||
if (!$user) {
|
||||
return response()->view('emails.verification-error', [
|
||||
'message' => 'Pengguna tidak ditemukan.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
if ($user->hasVerifiedEmail()) {
|
||||
return response()->view('emails.verification-success', [
|
||||
'message' => 'Email sudah diverifikasi sebelumnya.',
|
||||
'user' => $user
|
||||
]);
|
||||
}
|
||||
|
||||
if ($user->markEmailAsVerified()) {
|
||||
event(new Verified($user));
|
||||
}
|
||||
|
||||
return response()->view('emails.verification-success', [
|
||||
'message' => 'Email berhasil diverifikasi.',
|
||||
'user' => $user
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend verification email
|
||||
*/
|
||||
public function resend(Request $request)
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return $this->sendResponse([], 'Email sudah terverifikasi.');
|
||||
}
|
||||
|
||||
// Generate verification URL
|
||||
$verificationUrl = $this->generateVerificationUrl($request->user());
|
||||
|
||||
// Send custom verification email
|
||||
$request->user()->notify(new \App\Notifications\VerifyEmailNotification($verificationUrl));
|
||||
|
||||
return $this->sendResponse([
|
||||
'verification_url' => $verificationUrl
|
||||
], 'Link verifikasi email telah dikirim ulang.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate verification URL
|
||||
*/
|
||||
protected function generateVerificationUrl($user)
|
||||
{
|
||||
// Generate verification URL yang langsung mengarah ke API backend
|
||||
return URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60), // Link expires in 60 minutes
|
||||
['id' => $user->id, 'hash' => sha1($user->email)]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class ForgotPasswordController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Send reset password link
|
||||
*/
|
||||
public function sendResetLinkEmail(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
// Check if user exists and has the correct role based on the endpoint
|
||||
$user = User::where('email', $request->email)->first();
|
||||
$path = request()->path();
|
||||
$requiredRole = str_contains($path, 'penjahit') ? 'penjahit' : 'pelanggan';
|
||||
|
||||
if (!$user || $user->role !== $requiredRole) {
|
||||
return $this->sendError('Error.', ['email' => 'Email tidak ditemukan'], 404);
|
||||
}
|
||||
|
||||
// Generate 6-digit PIN
|
||||
$pin = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
|
||||
// Store PIN in password_resets table
|
||||
DB::table('password_resets')->updateOrInsert(
|
||||
['email' => $user->email],
|
||||
[
|
||||
'token' => $pin,
|
||||
'created_at' => Carbon::now()
|
||||
]
|
||||
);
|
||||
|
||||
// Send custom reset password email with PIN
|
||||
$user->notify(new \App\Notifications\ResetPasswordNotification($pin));
|
||||
|
||||
return $this->sendResponse([
|
||||
'message' => 'PIN reset password telah dikirim ke email Anda'
|
||||
], 'Kami sudah mengirim PIN reset password ke email Anda');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password
|
||||
*/
|
||||
public function reset(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'pin' => 'required|digits:6',
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|confirmed|min:8',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
// Check if user exists and has the correct role based on the endpoint
|
||||
$user = User::where('email', $request->email)->first();
|
||||
$path = request()->path();
|
||||
$requiredRole = str_contains($path, 'penjahit') ? 'penjahit' : 'pelanggan';
|
||||
|
||||
if (!$user || $user->role !== $requiredRole) {
|
||||
return $this->sendError('Error.', ['email' => 'Email tidak ditemukan'], 404);
|
||||
}
|
||||
|
||||
// Check if PIN exists and is valid
|
||||
$passwordReset = DB::table('password_resets')
|
||||
->where('email', $user->email)
|
||||
->where('token', $request->pin)
|
||||
->first();
|
||||
|
||||
if (!$passwordReset) {
|
||||
return $this->sendError('Error.', ['pin' => 'PIN tidak valid atau sudah kadaluarsa'], 400);
|
||||
}
|
||||
|
||||
// Check if PIN is expired (60 minutes)
|
||||
$createdAt = Carbon::parse($passwordReset->created_at);
|
||||
if ($createdAt->addMinutes(60)->isPast()) {
|
||||
DB::table('password_resets')->where('email', $user->email)->delete();
|
||||
return $this->sendError('Error.', ['pin' => 'PIN sudah kadaluarsa'], 400);
|
||||
}
|
||||
|
||||
// Update password
|
||||
$user->password = Hash::make($request->password);
|
||||
$user->save();
|
||||
|
||||
// Delete PIN
|
||||
DB::table('password_resets')->where('email', $user->email)->delete();
|
||||
|
||||
// Fire password reset event
|
||||
event(new PasswordReset($user));
|
||||
|
||||
return $this->sendResponse([], 'Password berhasil direset');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate reset password URL
|
||||
*/
|
||||
protected function generateResetUrl($user, $token)
|
||||
{
|
||||
// Get frontend URL from config or env
|
||||
$frontendUrl = config('app.frontend_url', env('FRONTEND_URL', 'http://localhost:3000'));
|
||||
|
||||
// Combine with frontend URL
|
||||
return $frontendUrl . '/reset-password?email=' . urlencode($user->email) . '&token=' . $token;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,330 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Booking;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Midtrans\Config;
|
||||
use Midtrans\Snap;
|
||||
use Midtrans\Notification;
|
||||
use Midtrans\Transaction;
|
||||
use App\Models\Wallet;
|
||||
|
||||
class MidtransController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Konfigurasi Midtrans
|
||||
Config::$serverKey = env('MIDTRANS_SERVER_KEY', 'SB-Mid-server-m_cMr-8mOqJoaorKXpXoWBoQ');
|
||||
Config::$isProduction = env('MIDTRANS_IS_PRODUCTION', false);
|
||||
Config::$isSanitized = true;
|
||||
Config::$is3ds = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inisiasi pembayaran dengan Midtrans
|
||||
*/
|
||||
public function initiatePayment(Request $request, Booking $booking)
|
||||
{
|
||||
try {
|
||||
// Periksa otentikasi dan otorisasi
|
||||
if (Auth::id() !== $booking->customer_id) {
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk melakukan pembayaran ini'], 403);
|
||||
}
|
||||
|
||||
// Periksa status booking
|
||||
if (!in_array($booking->status, ['reservasi', 'diproses', 'selesai'])) {
|
||||
return $this->sendError('Invalid status', ['error' => 'Pesanan tidak dapat dibayar dengan status saat ini'], 422);
|
||||
}
|
||||
|
||||
// Periksa jika pembayaran sudah dilakukan
|
||||
if (in_array($booking->payment_status, ['paid', 'success'])) {
|
||||
return $this->sendError('Already paid', ['error' => 'Pesanan ini sudah dibayar'], 422);
|
||||
}
|
||||
|
||||
// Pastikan harga telah diatur
|
||||
if (!$booking->total_price) {
|
||||
return $this->sendError('Invalid price', ['error' => 'Harga belum ditetapkan oleh penjahit'], 422);
|
||||
}
|
||||
|
||||
// Set up parameter untuk Midtrans
|
||||
$params = [
|
||||
'transaction_details' => [
|
||||
'order_id' => $booking->transaction_code, // Gunakan transaction_code yang sudah ada
|
||||
'gross_amount' => (int) ($booking->total_price + 4000), // Tambahkan biaya admin
|
||||
],
|
||||
'customer_details' => [
|
||||
'first_name' => $booking->customer->name,
|
||||
'email' => $booking->customer->email,
|
||||
'phone' => $booking->customer->phone_number ?? '',
|
||||
],
|
||||
'item_details' => [
|
||||
[
|
||||
'id' => 'BOOKING-' . $booking->id,
|
||||
'price' => (int) $booking->total_price,
|
||||
'quantity' => 1,
|
||||
'name' => 'Booking ' . $booking->service_type . ' - ' . $booking->category,
|
||||
],
|
||||
[
|
||||
'id' => 'ADMIN-FEE',
|
||||
'price' => 4000,
|
||||
'quantity' => 1,
|
||||
'name' => 'Biaya Admin (Payment Gateway + TailorHub)',
|
||||
]
|
||||
],
|
||||
// CATATAN PENTING: Jangan aktifkan baris di bawah ini jika ingin menampilkan semua metode pembayaran
|
||||
// Jika ingin membatasi metode pembayaran tertentu, uncomment baris di bawah ini
|
||||
// 'enabled_payments' => ['bank_transfer'],
|
||||
];
|
||||
|
||||
// Buat token transaksi Snap
|
||||
$snapToken = Snap::getSnapToken($params);
|
||||
|
||||
// Simpan informasi pembayaran
|
||||
$booking->update([
|
||||
'payment_method' => 'transfer_bank_midtrans',
|
||||
'midtrans_snap_token' => $snapToken
|
||||
]);
|
||||
|
||||
return $this->sendResponse([
|
||||
'snap_token' => $snapToken,
|
||||
'redirect_url' => 'https://app.sandbox.midtrans.com/snap/v2/vtweb/' . $snapToken,
|
||||
'booking' => [
|
||||
'id' => $booking->id,
|
||||
'transaction_code' => $booking->transaction_code,
|
||||
'total_price' => $booking->total_price,
|
||||
'status' => $booking->status,
|
||||
]
|
||||
], 'Token pembayaran berhasil dibuat');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Error initiating Midtrans payment: ' . $e->getMessage());
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat memulai pembayaran: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback notification handler dari Midtrans
|
||||
*/
|
||||
public function notificationHandler(Request $request)
|
||||
{
|
||||
try {
|
||||
$notificationBody = $request->all();
|
||||
\Log::info('Midtrans Notification Received', $notificationBody);
|
||||
|
||||
// Verifikasi signature menggunakan server key untuk keamanan
|
||||
$notification = new Notification();
|
||||
|
||||
$transactionStatus = $notification->transaction_status;
|
||||
$type = $notification->payment_type;
|
||||
$orderId = $notification->order_id;
|
||||
$fraud = $notification->fraud_status;
|
||||
|
||||
// Cari booking berdasarkan transaction_code (order_id)
|
||||
$booking = Booking::where('transaction_code', $orderId)->first();
|
||||
|
||||
if (!$booking) {
|
||||
return response()->json(['status' => 'error', 'message' => 'Booking tidak ditemukan'], 404);
|
||||
}
|
||||
|
||||
// Proses berdasarkan status transaksi
|
||||
if ($transactionStatus == 'capture') {
|
||||
if ($type == 'credit_card'){
|
||||
if($fraud == 'challenge'){
|
||||
$booking->payment_status = 'pending';
|
||||
} else {
|
||||
$booking->payment_status = 'paid';
|
||||
// Tambahkan dana ke wallet penjahit
|
||||
$tailorWallet = $booking->tailor->wallet;
|
||||
if (!$tailorWallet) {
|
||||
$tailorWallet = Wallet::create([
|
||||
'user_id' => $booking->tailor_id,
|
||||
'balance' => 0
|
||||
]);
|
||||
}
|
||||
$tailorWallet->addBalance(
|
||||
$booking->total_price,
|
||||
'Pembayaran booking #' . $booking->transaction_code,
|
||||
$booking
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ($transactionStatus == 'settlement') {
|
||||
$booking->payment_status = 'paid';
|
||||
// Tambahkan dana ke wallet penjahit
|
||||
$tailorWallet = $booking->tailor->wallet;
|
||||
if (!$tailorWallet) {
|
||||
$tailorWallet = Wallet::create([
|
||||
'user_id' => $booking->tailor_id,
|
||||
'balance' => 0
|
||||
]);
|
||||
}
|
||||
$tailorWallet->addBalance(
|
||||
$booking->total_price,
|
||||
'Pembayaran booking #' . $booking->transaction_code,
|
||||
$booking
|
||||
);
|
||||
}
|
||||
else if ($transactionStatus == 'pending') {
|
||||
$booking->payment_status = 'pending';
|
||||
}
|
||||
else if ($transactionStatus == 'deny') {
|
||||
$booking->payment_status = 'failed';
|
||||
}
|
||||
else if ($transactionStatus == 'expire') {
|
||||
$booking->payment_status = 'expired';
|
||||
}
|
||||
else if ($transactionStatus == 'cancel') {
|
||||
$booking->payment_status = 'cancelled';
|
||||
}
|
||||
|
||||
// Simpan perubahan
|
||||
$booking->save();
|
||||
|
||||
\Log::info('Payment status updated: ' . $booking->payment_status);
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Notification processed']);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Error processing Midtrans notification: ' . $e->getMessage());
|
||||
return response()->json(['status' => 'error', 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Memeriksa status pembayaran
|
||||
*/
|
||||
public function checkPaymentStatus(Booking $booking)
|
||||
{
|
||||
try {
|
||||
// Periksa otentikasi dan otorisasi
|
||||
if (Auth::id() !== $booking->customer_id && Auth::id() !== $booking->tailor_id) {
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk melihat status pembayaran ini'], 403);
|
||||
}
|
||||
|
||||
return $this->sendResponse([
|
||||
'booking_id' => $booking->id,
|
||||
'transaction_code' => $booking->transaction_code,
|
||||
'payment_status' => $booking->payment_status,
|
||||
'payment_method' => $booking->payment_method,
|
||||
'total_price' => $booking->total_price,
|
||||
'status' => $booking->status,
|
||||
'midtrans_snap_token' => $booking->midtrans_snap_token
|
||||
], 'Status pembayaran berhasil diambil');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Error checking payment status: ' . $e->getMessage());
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat memeriksa status pembayaran'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Memeriksa dan memperbarui status pembayaran secara manual
|
||||
* Dapat digunakan sebagai alternatif notification handler Midtrans
|
||||
*/
|
||||
public function manualCheckStatus(Booking $booking)
|
||||
{
|
||||
try {
|
||||
// Periksa otentikasi dan otorisasi
|
||||
// User harus customer atau penjahit terkait atau admin
|
||||
$user = Auth::user();
|
||||
if ($user->id !== $booking->customer_id && $user->id !== $booking->tailor_id && !$user->hasRole('admin')) {
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk memeriksa status pembayaran ini'], 403);
|
||||
}
|
||||
|
||||
// Periksa apakah booking memiliki transaction_code
|
||||
if (!$booking->transaction_code) {
|
||||
return $this->sendError('Invalid order', ['error' => 'Booking ini tidak memiliki kode transaksi'], 422);
|
||||
}
|
||||
|
||||
// Periksa status transaksi di Midtrans
|
||||
$statusResponse = Transaction::status($booking->transaction_code);
|
||||
|
||||
\Log::info('Midtrans Status Check Response', (array) $statusResponse);
|
||||
|
||||
$transactionStatus = $statusResponse->transaction_status ?? null;
|
||||
$fraudStatus = $statusResponse->fraud_status ?? null;
|
||||
$paymentType = $statusResponse->payment_type ?? null;
|
||||
|
||||
// Jika status sudah paid, tidak perlu update lagi
|
||||
if (in_array($booking->payment_status, ['paid', 'success']) && $transactionStatus == 'settlement') {
|
||||
return $this->sendResponse([
|
||||
'booking_id' => $booking->id,
|
||||
'transaction_code' => $booking->transaction_code,
|
||||
'payment_status' => $booking->payment_status,
|
||||
'payment_method' => $booking->payment_method,
|
||||
'midtrans_status' => $transactionStatus,
|
||||
], 'Pembayaran sudah berhasil');
|
||||
}
|
||||
|
||||
// Update status pembayaran berdasarkan response
|
||||
$statusUpdated = false;
|
||||
|
||||
if ($transactionStatus == 'capture') {
|
||||
if ($paymentType == 'credit_card') {
|
||||
if ($fraudStatus == 'challenge') {
|
||||
$booking->payment_status = 'pending';
|
||||
} else {
|
||||
$booking->payment_status = 'paid';
|
||||
$statusUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ($transactionStatus == 'settlement') {
|
||||
$booking->payment_status = 'paid';
|
||||
$statusUpdated = true;
|
||||
}
|
||||
else if ($transactionStatus == 'pending') {
|
||||
$booking->payment_status = 'pending';
|
||||
}
|
||||
else if ($transactionStatus == 'deny') {
|
||||
$booking->payment_status = 'failed';
|
||||
}
|
||||
else if ($transactionStatus == 'expire') {
|
||||
$booking->payment_status = 'expired';
|
||||
}
|
||||
else if ($transactionStatus == 'cancel') {
|
||||
$booking->payment_status = 'cancelled';
|
||||
}
|
||||
|
||||
// Jika status berubah menjadi paid, tambahkan dana ke wallet penjahit
|
||||
if ($statusUpdated && $booking->payment_status == 'paid') {
|
||||
$tailorWallet = $booking->tailor->wallet;
|
||||
if (!$tailorWallet) {
|
||||
$tailorWallet = Wallet::create([
|
||||
'user_id' => $booking->tailor_id,
|
||||
'balance' => 0
|
||||
]);
|
||||
}
|
||||
$tailorWallet->addBalance(
|
||||
$booking->total_price,
|
||||
'Pembayaran booking #' . $booking->transaction_code,
|
||||
$booking
|
||||
);
|
||||
}
|
||||
|
||||
// Simpan perubahan
|
||||
$booking->save();
|
||||
|
||||
\Log::info('Payment status manually updated: ' . $booking->payment_status);
|
||||
|
||||
return $this->sendResponse([
|
||||
'booking_id' => $booking->id,
|
||||
'transaction_code' => $booking->transaction_code,
|
||||
'payment_status' => $booking->payment_status,
|
||||
'payment_method' => $booking->payment_method,
|
||||
'total_price' => $booking->total_price,
|
||||
'status' => $booking->status,
|
||||
'midtrans_status' => $transactionStatus,
|
||||
'midtrans_snap_token' => $booking->midtrans_snap_token
|
||||
], 'Status pembayaran berhasil diperbarui');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Error checking payment status manually: ' . $e->getMessage());
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat memeriksa status pembayaran: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\Booking;
|
||||
use App\Models\TailorRating;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class RatingController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Store a new rating
|
||||
*/
|
||||
public function store(Request $request, Booking $booking)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'rating' => 'required|numeric|min:0|max:5',
|
||||
'review' => 'nullable|string|max:1000'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if booking belongs to the authenticated user
|
||||
if ($booking->customer_id !== Auth::id()) {
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk memberikan rating pada booking ini'], 403);
|
||||
}
|
||||
|
||||
// Check if booking is completed
|
||||
if ($booking->status !== 'selesai') {
|
||||
return $this->sendError('Error validasi.', ['error' => 'Anda hanya dapat memberikan rating untuk pesanan yang telah selesai'], 422);
|
||||
}
|
||||
|
||||
// Check if rating already exists
|
||||
if (TailorRating::where('booking_id', $booking->id)->where('customer_id', Auth::id())->exists()) {
|
||||
return $this->sendError('Error validasi.', ['error' => 'Anda sudah memberikan rating untuk pesanan ini'], 422);
|
||||
}
|
||||
|
||||
$rating = TailorRating::create([
|
||||
'booking_id' => $booking->id,
|
||||
'customer_id' => Auth::id(),
|
||||
'tailor_id' => $booking->tailor_id,
|
||||
'rating' => $request->rating,
|
||||
'review' => $request->review
|
||||
]);
|
||||
|
||||
return $this->sendResponse($rating, 'Rating berhasil ditambahkan.');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat menambahkan rating'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tailor's ratings
|
||||
*/
|
||||
public function getTailorRatings($tailorId)
|
||||
{
|
||||
try {
|
||||
$ratings = TailorRating::where('tailor_id', $tailorId)
|
||||
->with(['customer:id,name', 'booking'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
$averageRating = $ratings->avg('rating');
|
||||
|
||||
return $this->sendResponse([
|
||||
'ratings' => $ratings,
|
||||
'average_rating' => round($averageRating, 1),
|
||||
'total_ratings' => $ratings->count()
|
||||
], 'Data rating berhasil diambil.');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil data rating'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a rating
|
||||
*/
|
||||
public function update(Request $request, TailorRating $rating)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'rating' => 'required|numeric|min:0|max:5',
|
||||
'review' => 'nullable|string|max:1000'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if rating belongs to the authenticated user
|
||||
if ($rating->customer_id !== Auth::id()) {
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk mengubah rating ini'], 403);
|
||||
}
|
||||
|
||||
$rating->update([
|
||||
'rating' => $request->rating,
|
||||
'review' => $request->review
|
||||
]);
|
||||
|
||||
return $this->sendResponse($rating, 'Rating berhasil diupdate.');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengupdate rating'], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\Booking;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class TailorCalendarController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Get calendar bookings for the month
|
||||
*/
|
||||
public function getCalendarBookings($month, $year)
|
||||
{
|
||||
try {
|
||||
// Log input parameters
|
||||
\Log::info('Calendar request:', [
|
||||
'month' => $month,
|
||||
'year' => $year,
|
||||
'user_id' => Auth::id(),
|
||||
'user_role' => Auth::user()->role
|
||||
]);
|
||||
|
||||
// Validate month and year
|
||||
if (!is_numeric($month) || !is_numeric($year) ||
|
||||
$month < 1 || $month > 12 ||
|
||||
$year < 2024 || $year > 2100) {
|
||||
\Log::warning('Invalid date parameters', [
|
||||
'month' => $month,
|
||||
'year' => $year
|
||||
]);
|
||||
return $this->sendError('Invalid date', ['error' => 'Bulan atau tahun tidak valid'], 422);
|
||||
}
|
||||
|
||||
// Create Carbon instances for start and end of month
|
||||
try {
|
||||
$startOfMonth = Carbon::create($year, $month, 1)->startOfMonth();
|
||||
$endOfMonth = Carbon::create($year, $month, 1)->endOfMonth();
|
||||
|
||||
\Log::info('Date range:', [
|
||||
'start' => $startOfMonth->format('Y-m-d'),
|
||||
'end' => $endOfMonth->format('Y-m-d')
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Date creation error:', [
|
||||
'message' => $e->getMessage(),
|
||||
'month' => $month,
|
||||
'year' => $year
|
||||
]);
|
||||
return $this->sendError('Invalid date', ['error' => 'Format tanggal tidak valid'], 422);
|
||||
}
|
||||
|
||||
// Get all bookings for the month
|
||||
$bookings = Booking::where('tailor_id', Auth::id())
|
||||
->whereBetween('appointment_date', [
|
||||
$startOfMonth->format('Y-m-d'),
|
||||
$endOfMonth->format('Y-m-d')
|
||||
])
|
||||
->with(['customer:id,name,phone_number'])
|
||||
->orderBy('appointment_date')
|
||||
->orderBy('appointment_time')
|
||||
->get();
|
||||
|
||||
\Log::info('Bookings found:', [
|
||||
'count' => $bookings->count(),
|
||||
'date_range' => [
|
||||
$startOfMonth->format('Y-m-d'),
|
||||
$endOfMonth->format('Y-m-d')
|
||||
]
|
||||
]);
|
||||
|
||||
// Initialize calendar array
|
||||
$calendar = [];
|
||||
$currentDate = $startOfMonth->copy();
|
||||
|
||||
// Initialize all dates of the month
|
||||
while ($currentDate <= $endOfMonth) {
|
||||
$dateStr = $currentDate->format('Y-m-d');
|
||||
$calendar[$dateStr] = [
|
||||
'date' => $dateStr,
|
||||
'day' => $currentDate->format('d'),
|
||||
'day_name' => $currentDate->isoFormat('dddd'),
|
||||
'bookings' => []
|
||||
];
|
||||
$currentDate->addDay();
|
||||
}
|
||||
|
||||
// Add bookings to their respective dates
|
||||
foreach ($bookings as $booking) {
|
||||
$date = Carbon::parse($booking->appointment_date)->format('Y-m-d');
|
||||
if (isset($calendar[$date])) {
|
||||
$calendar[$date]['bookings'][] = [
|
||||
'id' => $booking->id,
|
||||
'time' => $booking->appointment_time,
|
||||
'customer_name' => $booking->customer->name,
|
||||
'customer_phone' => $booking->customer->phone_number,
|
||||
'service_type' => $booking->service_type,
|
||||
'category' => $booking->category,
|
||||
'status' => $booking->status,
|
||||
'payment_status' => $booking->payment_status
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Get month summary
|
||||
$summary = [
|
||||
'total_bookings' => $bookings->count(),
|
||||
'pending_bookings' => $bookings->where('status', 'reservasi')->count(),
|
||||
'ongoing_bookings' => $bookings->where('status', 'diproses')->count(),
|
||||
'completed_bookings' => $bookings->where('status', 'selesai')->count(),
|
||||
'cancelled_bookings' => $bookings->where('status', 'dibatalkan')->count(),
|
||||
];
|
||||
|
||||
\Log::info('Calendar response prepared', [
|
||||
'total_dates' => count($calendar),
|
||||
'summary' => $summary
|
||||
]);
|
||||
|
||||
return $this->sendResponse([
|
||||
'month' => (int)$month,
|
||||
'year' => (int)$year,
|
||||
'month_name' => Carbon::create($year, $month, 1)->isoFormat('MMMM'),
|
||||
'calendar' => array_values($calendar),
|
||||
'summary' => $summary
|
||||
], 'Data kalender berhasil diambil');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Calendar error:', [
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil data kalender: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bookings for specific date
|
||||
*/
|
||||
public function getDateBookings($date)
|
||||
{
|
||||
try {
|
||||
// Validate date format
|
||||
try {
|
||||
$bookingDate = Carbon::parse($date);
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Invalid date', ['error' => 'Format tanggal tidak valid'], 422);
|
||||
}
|
||||
|
||||
// Get all bookings for the date
|
||||
$bookings = Booking::where('tailor_id', Auth::id())
|
||||
->whereDate('appointment_date', $bookingDate->format('Y-m-d'))
|
||||
->with([
|
||||
'customer:id,name,phone_number,address'
|
||||
])
|
||||
->orderBy('appointment_time')
|
||||
->get();
|
||||
|
||||
// Format bookings with detailed information
|
||||
$formattedBookings = $bookings->map(function ($booking) {
|
||||
return [
|
||||
'id' => $booking->id,
|
||||
'transaction_code' => $booking->transaction_code,
|
||||
'time' => $booking->appointment_time,
|
||||
'customer' => [
|
||||
'name' => $booking->customer->name,
|
||||
'phone' => $booking->customer->phone_number,
|
||||
'address' => $booking->customer->address
|
||||
],
|
||||
'service_type' => $booking->service_type,
|
||||
'category' => $booking->category,
|
||||
'status' => $booking->status,
|
||||
'payment_status' => $booking->payment_status,
|
||||
'payment_method' => $booking->payment_method,
|
||||
'notes' => $booking->notes,
|
||||
'measurements' => $booking->measurements,
|
||||
'total_price' => $booking->total_price,
|
||||
'design_photo' => $booking->design_photo ? url('storage/designs/' . $booking->design_photo) : null,
|
||||
'completion_photo' => $booking->completion_photo ? url('storage/completions/' . $booking->completion_photo) : null
|
||||
];
|
||||
});
|
||||
|
||||
// Get day summary
|
||||
$summary = [
|
||||
'total_bookings' => $bookings->count(),
|
||||
'pending_bookings' => $bookings->where('status', 'reservasi')->count(),
|
||||
'ongoing_bookings' => $bookings->where('status', 'diproses')->count(),
|
||||
'completed_bookings' => $bookings->where('status', 'selesai')->count(),
|
||||
'cancelled_bookings' => $bookings->where('status', 'dibatalkan')->count(),
|
||||
];
|
||||
|
||||
return $this->sendResponse([
|
||||
'date' => $bookingDate->format('Y-m-d'),
|
||||
'day_name' => $bookingDate->isoFormat('dddd'),
|
||||
'bookings' => $formattedBookings,
|
||||
'summary' => $summary
|
||||
], 'Data pesanan tanggal ' . $bookingDate->format('Y-m-d') . ' berhasil diambil');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Calendar date error: ' . $e->getMessage());
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil data pesanan: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,268 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TailorController extends Controller
|
||||
{
|
||||
public function getAllTailors()
|
||||
{
|
||||
try {
|
||||
$tailors = User::where('role', 'penjahit')
|
||||
->withAvg('ratings as average_rating', 'rating')
|
||||
->having('average_rating', '>', 2.5)
|
||||
->orWhereNull('average_rating')
|
||||
->with(['specializations', 'services', 'ratings'])
|
||||
->get()
|
||||
->map(function ($tailor) {
|
||||
return [
|
||||
'id' => $tailor->id,
|
||||
'name' => $tailor->name,
|
||||
'email' => $tailor->email,
|
||||
'phone' => $tailor->phone_number,
|
||||
'profile' => [
|
||||
'shop_name' => $tailor->name,
|
||||
'address' => $tailor->address,
|
||||
'description' => $tailor->shop_description,
|
||||
'photo' => $tailor->profile_photo,
|
||||
'latitude' => $tailor->latitude,
|
||||
'longitude' => $tailor->longitude
|
||||
],
|
||||
'specializations' => $tailor->specializations->map(function ($specialization) {
|
||||
return [
|
||||
'id' => $specialization->id,
|
||||
'name' => $specialization->name,
|
||||
'photo' => $specialization->photo
|
||||
];
|
||||
}),
|
||||
'services' => $tailor->services->map(function ($service) {
|
||||
return [
|
||||
'id' => $service->id,
|
||||
'name' => $service->name,
|
||||
'price' => $service->price,
|
||||
'description' => $service->description,
|
||||
'is_available' => $service->is_available
|
||||
];
|
||||
}),
|
||||
'ratings' => [
|
||||
'average_rating' => round($tailor->average_rating ?? 0, 1),
|
||||
'total_ratings' => $tailor->ratings->count(),
|
||||
'reviews' => $tailor->ratings->map(function ($rating) {
|
||||
return [
|
||||
'rating' => $rating->rating,
|
||||
'comment' => $rating->review,
|
||||
'created_at' => $rating->created_at
|
||||
];
|
||||
})
|
||||
]
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $tailors
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Failed to retrieve tailors data',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Method untuk mendapatkan semua penjahit tanpa filter rating
|
||||
public function getAllTailorsNoFilter()
|
||||
{
|
||||
try {
|
||||
$tailors = User::where('role', 'penjahit')
|
||||
->withAvg('ratings as average_rating', 'rating')
|
||||
->with(['specializations', 'services', 'ratings'])
|
||||
->get()
|
||||
->map(function ($tailor) {
|
||||
return [
|
||||
'id' => $tailor->id,
|
||||
'name' => $tailor->name,
|
||||
'email' => $tailor->email,
|
||||
'phone' => $tailor->phone_number,
|
||||
'profile' => [
|
||||
'shop_name' => $tailor->name,
|
||||
'address' => $tailor->address,
|
||||
'description' => $tailor->shop_description,
|
||||
'photo' => $tailor->profile_photo,
|
||||
'latitude' => $tailor->latitude,
|
||||
'longitude' => $tailor->longitude
|
||||
],
|
||||
'specializations' => $tailor->specializations->map(function ($specialization) {
|
||||
return [
|
||||
'id' => $specialization->id,
|
||||
'name' => $specialization->name,
|
||||
'photo' => $specialization->photo
|
||||
];
|
||||
}),
|
||||
'services' => $tailor->services->map(function ($service) {
|
||||
return [
|
||||
'id' => $service->id,
|
||||
'name' => $service->name,
|
||||
'price' => $service->price,
|
||||
'description' => $service->description,
|
||||
'is_available' => $service->is_available
|
||||
];
|
||||
}),
|
||||
'ratings' => [
|
||||
'average_rating' => round($tailor->average_rating ?? 0, 1),
|
||||
'total_ratings' => $tailor->ratings->count(),
|
||||
'reviews' => $tailor->ratings->map(function ($rating) {
|
||||
return [
|
||||
'rating' => $rating->rating,
|
||||
'comment' => $rating->review,
|
||||
'created_at' => $rating->created_at
|
||||
];
|
||||
})
|
||||
]
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $tailors
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Failed to retrieve tailors data',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteTailor($id)
|
||||
{
|
||||
try {
|
||||
$tailor = User::where('role', 'penjahit')->find($id);
|
||||
|
||||
if (!$tailor) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Penjahit tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Hapus relasi spesialisasi terlebih dahulu
|
||||
$tailor->specializations()->detach();
|
||||
|
||||
// Hapus relasi gallery
|
||||
$tailor->gallery()->delete();
|
||||
|
||||
// Hapus relasi services
|
||||
$tailor->services()->delete();
|
||||
|
||||
// Hapus relasi ratings
|
||||
$tailor->ratings()->delete();
|
||||
|
||||
// Hapus penjahit
|
||||
$tailor->delete();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Penjahit berhasil dihapus'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Gagal menghapus penjahit',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function updateTailor(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
// Validasi input
|
||||
$request->validate([
|
||||
'name' => 'sometimes|string|max:255',
|
||||
'email' => 'sometimes|email|unique:users,email,' . $id,
|
||||
'phone_number' => 'sometimes|string|max:20',
|
||||
'address' => 'sometimes|string',
|
||||
'shop_description' => 'sometimes|string',
|
||||
'latitude' => 'sometimes|numeric',
|
||||
'longitude' => 'sometimes|numeric',
|
||||
'specialization_ids' => 'sometimes|array',
|
||||
'specialization_ids.*' => 'exists:tailor_specializations,id'
|
||||
]);
|
||||
|
||||
// Cari penjahit
|
||||
$tailor = User::where('role', 'penjahit')->find($id);
|
||||
|
||||
if (!$tailor) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Penjahit tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Update data dasar
|
||||
$tailor->update($request->only([
|
||||
'name',
|
||||
'email',
|
||||
'phone_number',
|
||||
'address',
|
||||
'shop_description',
|
||||
'latitude',
|
||||
'longitude'
|
||||
]));
|
||||
|
||||
// Update spesialisasi jika ada
|
||||
if ($request->has('specialization_ids')) {
|
||||
$tailor->specializations()->sync($request->specialization_ids);
|
||||
}
|
||||
|
||||
// Load ulang relasi untuk memastikan data terbaru
|
||||
$tailor->load('specializations');
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Data penjahit berhasil diperbarui',
|
||||
'data' => [
|
||||
'id' => $tailor->id,
|
||||
'name' => $tailor->name,
|
||||
'email' => $tailor->email,
|
||||
'phone_number' => $tailor->phone_number,
|
||||
'address' => $tailor->address,
|
||||
'shop_description' => $tailor->shop_description,
|
||||
'profile_photo' => $tailor->profile_photo,
|
||||
'location' => [
|
||||
'latitude' => $tailor->latitude,
|
||||
'longitude' => $tailor->longitude
|
||||
],
|
||||
'specializations' => $tailor->specializations->map(function ($spec) {
|
||||
return [
|
||||
'id' => $spec->id,
|
||||
'name' => $spec->name,
|
||||
'icon' => $spec->icon
|
||||
];
|
||||
})
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Validasi gagal',
|
||||
'errors' => $e->errors()
|
||||
], 422);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Gagal memperbarui data penjahit',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\Booking;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class TailorDashboardController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Get tailor dashboard data
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
$tailor_id = Auth::id();
|
||||
$now = Carbon::now();
|
||||
|
||||
// Get incoming bookings (status 'pending')
|
||||
$incomingBookings = Booking::where('tailor_id', $tailor_id)
|
||||
->where('status', 'pending')
|
||||
->with(['customer:id,name', 'customer.ratings'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
// Calculate current month's earnings
|
||||
$currentMonthEarnings = Booking::where('tailor_id', $tailor_id)
|
||||
->where('status', 'selesai')
|
||||
->where('payment_status', 'paid')
|
||||
->whereYear('updated_at', $now->year)
|
||||
->whereMonth('updated_at', $now->month)
|
||||
->sum('total_price');
|
||||
|
||||
// Calculate total earnings
|
||||
$totalEarnings = Booking::where('tailor_id', $tailor_id)
|
||||
->where('status', 'selesai')
|
||||
->where('payment_status', 'paid')
|
||||
->sum('total_price');
|
||||
|
||||
// Get average rating
|
||||
$averageRating = DB::table('tailor_ratings')
|
||||
->where('tailor_id', $tailor_id)
|
||||
->avg('rating');
|
||||
|
||||
return $this->sendResponse([
|
||||
'incoming_bookings' => $incomingBookings,
|
||||
'current_month_earnings' => $currentMonthEarnings,
|
||||
'total_earnings' => $totalEarnings,
|
||||
'average_rating' => round($averageRating ?? 0, 1),
|
||||
'total_completed_orders' => Booking::where('tailor_id', $tailor_id)
|
||||
->where('status', 'selesai')
|
||||
->count()
|
||||
], 'Data dashboard berhasil diambil');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil data dashboard'], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\TailorGallery;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class TailorGalleryController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Get gallery items for the authenticated tailor
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
$gallery = TailorGallery::where('user_id', Auth::id())
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return $this->sendResponse($gallery, 'Data galeri berhasil diambil');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil data galeri'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new gallery item
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
try {
|
||||
// Log request data
|
||||
\Log::info('Attempting to store gallery photo', [
|
||||
'user_id' => Auth::id(),
|
||||
'has_file' => $request->hasFile('photo'),
|
||||
'all_data' => $request->all()
|
||||
]);
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'photo' => 'required|image|mimes:jpeg,png,jpg|max:2048',
|
||||
'title' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'category' => 'nullable|string|max:100'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
\Log::warning('Gallery photo validation failed', [
|
||||
'errors' => $validator->errors()->toArray()
|
||||
]);
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
// Upload dan simpan foto
|
||||
$photo = $request->file('photo');
|
||||
|
||||
\Log::info('Photo details', [
|
||||
'original_name' => $photo->getClientOriginalName(),
|
||||
'mime_type' => $photo->getMimeType(),
|
||||
'size' => $photo->getSize()
|
||||
]);
|
||||
|
||||
$fileName = 'gallery_' . time() . '_' . Auth::id() . '.' . $photo->getClientOriginalExtension();
|
||||
|
||||
try {
|
||||
$photo->storeAs('gallery_photos', $fileName, 'public');
|
||||
\Log::info('Photo stored successfully', ['filename' => $fileName]);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Failed to store photo', [
|
||||
'error' => $e->getMessage(),
|
||||
'filename' => $fileName
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Buat record galeri baru
|
||||
$gallery = TailorGallery::create([
|
||||
'user_id' => Auth::id(),
|
||||
'photo' => '/storage/gallery_photos/' . $fileName,
|
||||
'title' => $request->title,
|
||||
'description' => $request->description,
|
||||
'category' => $request->category
|
||||
]);
|
||||
|
||||
\Log::info('Gallery record created successfully', ['gallery_id' => $gallery->id]);
|
||||
|
||||
return $this->sendResponse($gallery, 'Foto berhasil ditambahkan ke galeri');
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Error storing gallery photo', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat menambah foto: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update gallery item details
|
||||
*/
|
||||
public function update(Request $request, TailorGallery $gallery)
|
||||
{
|
||||
try {
|
||||
// Check ownership
|
||||
if ($gallery->user_id !== Auth::id()) {
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk mengubah foto ini'], 403);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'title' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'category' => 'nullable|string|max:100'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
$gallery->update($request->only(['title', 'description', 'category']));
|
||||
|
||||
return $this->sendResponse($gallery, 'Data galeri berhasil diupdate');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengupdate data'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete gallery item
|
||||
*/
|
||||
public function destroy(TailorGallery $gallery)
|
||||
{
|
||||
try {
|
||||
// Check ownership
|
||||
if ($gallery->user_id !== Auth::id()) {
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk menghapus foto ini'], 403);
|
||||
}
|
||||
|
||||
// Delete photo file
|
||||
$photoPath = str_replace('/storage/', '', $gallery->photo);
|
||||
if (Storage::disk('public')->exists($photoPath)) {
|
||||
Storage::disk('public')->delete($photoPath);
|
||||
}
|
||||
|
||||
$gallery->delete();
|
||||
|
||||
return $this->sendResponse(null, 'Foto berhasil dihapus dari galeri');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat menghapus foto'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get gallery items for a specific tailor (public)
|
||||
*/
|
||||
public function getTailorGallery($tailorId)
|
||||
{
|
||||
try {
|
||||
$gallery = TailorGallery::where('user_id', $tailorId)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return $this->sendResponse($gallery, 'Data galeri penjahit berhasil diambil');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil data galeri'], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class TailorProfileController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Get tailor shop profile
|
||||
*/
|
||||
public function getProfile()
|
||||
{
|
||||
try {
|
||||
$user = Auth::user();
|
||||
$user->load('specializations');
|
||||
|
||||
return $this->sendResponse([
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'phone_number' => $user->phone_number,
|
||||
'address' => $user->address,
|
||||
'shop_description' => $user->shop_description,
|
||||
'profile_photo' => $user->profile_photo ? asset('storage/' . $user->profile_photo) : null,
|
||||
'specializations' => $user->specializations->map(function($spec) {
|
||||
return [
|
||||
'id' => $spec->id,
|
||||
'name' => $spec->name
|
||||
];
|
||||
})
|
||||
], 'Data profil toko berhasil diambil');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil data profil'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tailor shop profile
|
||||
*/
|
||||
public function updateProfile(Request $request)
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'phone_number' => 'required|string|max:15',
|
||||
'address' => 'required|string|max:500',
|
||||
'shop_description' => 'nullable|string|max:1000',
|
||||
'profile_photo' => 'nullable|image|mimes:jpeg,png,jpg|max:2048'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
// Handle profile photo upload
|
||||
if ($request->hasFile('profile_photo')) {
|
||||
// Delete old photo if exists
|
||||
if ($user->profile_photo) {
|
||||
Storage::disk('public')->delete($user->profile_photo);
|
||||
}
|
||||
|
||||
// Store new photo
|
||||
$path = $request->file('profile_photo')->store('profile_photos', 'public');
|
||||
$user->profile_photo = $path;
|
||||
}
|
||||
|
||||
// Update user data
|
||||
$user->name = $request->name;
|
||||
$user->phone_number = $request->phone_number;
|
||||
$user->address = $request->address;
|
||||
$user->shop_description = $request->shop_description;
|
||||
$user->save();
|
||||
|
||||
// Reload user with specializations
|
||||
$user->load('specializations');
|
||||
|
||||
return $this->sendResponse([
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'phone_number' => $user->phone_number,
|
||||
'address' => $user->address,
|
||||
'shop_description' => $user->shop_description,
|
||||
'profile_photo' => $user->profile_photo ? asset('storage/' . $user->profile_photo) : null,
|
||||
'specializations' => $user->specializations->map(function($spec) {
|
||||
return [
|
||||
'id' => $spec->id,
|
||||
'name' => $spec->name
|
||||
];
|
||||
})
|
||||
], 'Profil toko berhasil diupdate');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengupdate profil'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete profile photo
|
||||
*/
|
||||
public function deleteProfilePhoto()
|
||||
{
|
||||
try {
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user->profile_photo) {
|
||||
Storage::disk('public')->delete($user->profile_photo);
|
||||
$user->profile_photo = null;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
return $this->sendResponse(null, 'Foto profil berhasil dihapus');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat menghapus foto profil'], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,483 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\TailorSpecialization;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TailorSearchController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Search tailors by specialization
|
||||
*/
|
||||
public function searchBySpecialization(Request $request)
|
||||
{
|
||||
try {
|
||||
$query = User::where('role', 'penjahit')
|
||||
->with('specializations');
|
||||
|
||||
// Filter by specialization if provided
|
||||
if ($request->has('specialization_id') && $request->specialization_id) {
|
||||
$query->whereHas('specializations', function ($q) use ($request) {
|
||||
$q->where('tailor_specializations.id', $request->specialization_id);
|
||||
});
|
||||
}
|
||||
|
||||
// Get user's current peminatan if logged in as customer
|
||||
$userPreferred = [];
|
||||
$user = Auth::user();
|
||||
if ($user && $user->role === 'pelanggan') {
|
||||
$userPreferred = $user->preferredSpecializations->pluck('id')->toArray();
|
||||
}
|
||||
|
||||
// Get tailors
|
||||
$tailors = $query->get();
|
||||
|
||||
// Add distance if customer has coordinates
|
||||
if ($user && $user->role === 'pelanggan' && $user->latitude && $user->longitude) {
|
||||
foreach ($tailors as $tailor) {
|
||||
if ($tailor->latitude && $tailor->longitude) {
|
||||
$tailor->distance = $this->calculateDistance(
|
||||
$user->latitude,
|
||||
$user->longitude,
|
||||
$tailor->latitude,
|
||||
$tailor->longitude
|
||||
);
|
||||
} else {
|
||||
$tailor->distance = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by distance if available
|
||||
$tailors = $tailors->sortBy('distance');
|
||||
}
|
||||
|
||||
return $this->sendResponse([
|
||||
'tailors' => $tailors->values(),
|
||||
'user_preferred' => $userPreferred
|
||||
], 'Data penjahit berhasil diambil');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mencari penjahit: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommended tailors based on customer preferences using Content-Based Filtering
|
||||
*/
|
||||
public function getRecommended()
|
||||
{
|
||||
try {
|
||||
// Log untuk debug
|
||||
\Log::info('Accessing getRecommended method');
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
// Check if user is customer
|
||||
if (!$user || $user->role !== 'pelanggan') {
|
||||
\Log::warning('User not authorized: ', ['user' => $user ? $user->toArray() : 'null']);
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Anda harus login sebagai pelanggan'], 403);
|
||||
}
|
||||
|
||||
// Get user's preferred specializations
|
||||
$preferredSpecIds = $user->preferredSpecializations->pluck('id')->toArray();
|
||||
\Log::info('User preferred specializations: ', ['preferred' => $preferredSpecIds]);
|
||||
|
||||
// Get all available specializations for feature vector preparation
|
||||
$allSpecializations = TailorSpecialization::all()->pluck('id')->toArray();
|
||||
|
||||
// Get all tailors
|
||||
$allTailors = User::where('role', 'penjahit')
|
||||
->with(['specializations', 'ratings'])
|
||||
->get();
|
||||
|
||||
if (empty($preferredSpecIds)) {
|
||||
// If no preferences, return popular tailors
|
||||
\Log::info('No preferences found, returning popular tailors');
|
||||
$tailors = User::where('role', 'penjahit')
|
||||
->withCount([
|
||||
'bookings' => function ($query) {
|
||||
$query->where('status', 'selesai');
|
||||
}
|
||||
])
|
||||
->orderBy('bookings_count', 'desc')
|
||||
->with(['specializations', 'ratings'])
|
||||
->limit(10)
|
||||
->get();
|
||||
} else {
|
||||
|
||||
// Mengubah preferensi user menjadi vektor
|
||||
$userProfile = $this->createFeatureVector($preferredSpecIds, $allSpecializations);
|
||||
|
||||
// Calculate similarity scores for all tailors
|
||||
$tailorsWithScores = [];
|
||||
foreach ($allTailors as $tailor) {
|
||||
$tailorSpecIds = $tailor->specializations->pluck('id')->toArray();
|
||||
$tailorProfile = $this->createFeatureVector($tailorSpecIds, $allSpecializations);
|
||||
|
||||
// Calculate cosine similarity between user and tailor profiles
|
||||
$similarityScore = $this->calculateCosineSimilarity($userProfile, $tailorProfile);
|
||||
|
||||
// Only include tailors with some similarity
|
||||
if ($similarityScore > 0) {
|
||||
$tailor->similarity_score = $similarityScore;
|
||||
$tailorsWithScores[] = $tailor;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort tailors by similarity score
|
||||
$tailors = collect($tailorsWithScores)->sortByDesc('similarity_score');
|
||||
|
||||
// If no similar tailors found, return popular ones
|
||||
if ($tailors->isEmpty()) {
|
||||
$tailors = User::where('role', 'penjahit')
|
||||
->withCount([
|
||||
'bookings' => function ($query) {
|
||||
$query->where('status', 'selesai');
|
||||
}
|
||||
])
|
||||
->orderBy('bookings_count', 'desc')
|
||||
->with(['specializations', 'ratings'])
|
||||
->limit(10)
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
\Log::info('Tailors found: ', ['count' => $tailors->count()]);
|
||||
|
||||
// Add distance and rating information
|
||||
foreach ($tailors as $tailor) {
|
||||
// Calculate distance if coordinates available
|
||||
if ($user->latitude && $user->longitude && $tailor->latitude && $tailor->longitude) {
|
||||
$tailor->distance = $this->calculateDistance(
|
||||
$user->latitude,
|
||||
$user->longitude,
|
||||
$tailor->latitude,
|
||||
$tailor->longitude
|
||||
);
|
||||
} else {
|
||||
$tailor->distance = null;
|
||||
}
|
||||
|
||||
// Calculate average rating
|
||||
$ratings = $tailor->ratings;
|
||||
if ($ratings->count() > 0) {
|
||||
$tailor->rating_info = [
|
||||
'average_rating' => round($ratings->avg('rating'), 1),
|
||||
'total_reviews' => $ratings->count(),
|
||||
'reviews' => $ratings->map(function ($rating) {
|
||||
return [
|
||||
'rating' => $rating->rating,
|
||||
'review' => $rating->review,
|
||||
'created_at' => $rating->created_at,
|
||||
'customer' => [
|
||||
'id' => $rating->customer->id,
|
||||
'name' => $rating->customer->name,
|
||||
'profile_photo' => $rating->customer->profile_photo
|
||||
]
|
||||
];
|
||||
})
|
||||
];
|
||||
} else {
|
||||
$tailor->rating_info = [
|
||||
'average_rating' => 0,
|
||||
'total_reviews' => 0,
|
||||
'reviews' => []
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// If user has location, use hybrid approach combining similarity and distance
|
||||
if ($user->latitude && $user->longitude) {
|
||||
// Normalize scores for hybrid ranking
|
||||
$maxDistance = $tailors->max('distance') ?: 1;
|
||||
$tailors = $tailors->map(function ($tailor) use ($maxDistance) {
|
||||
// Calculate normalized distance score (1 when closest, 0 when furthest)
|
||||
if ($tailor->distance !== null) {
|
||||
$tailor->distance_score = 1 - ($tailor->distance / $maxDistance);
|
||||
} else {
|
||||
$tailor->distance_score = 0;
|
||||
}
|
||||
|
||||
// Hybrid score (70% similarity, 30% proximity)
|
||||
$tailor->hybrid_score = isset($tailor->similarity_score)
|
||||
? ($tailor->similarity_score * 0.7) + ($tailor->distance_score * 0.3)
|
||||
: $tailor->distance_score;
|
||||
|
||||
return $tailor;
|
||||
})->sortByDesc('hybrid_score');
|
||||
}
|
||||
|
||||
return $this->sendResponse([
|
||||
'tailors' => $tailors->values(),
|
||||
'user_preferred' => $preferredSpecIds
|
||||
], 'Rekomendasi penjahit berhasil diambil');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Content-Based Filtering error: ' . $e->getMessage() . "\n" . $e->getTraceAsString());
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mendapatkan rekomendasi: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create binary feature vector based on specializations
|
||||
*/
|
||||
private function createFeatureVector($specializations, $allSpecializations)
|
||||
{
|
||||
$vector = [];
|
||||
foreach ($allSpecializations as $specId) {
|
||||
$vector[$specId] = in_array($specId, $specializations) ? 1 : 0;
|
||||
}
|
||||
return $vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cosine similarity between two feature vectors
|
||||
*/
|
||||
private function calculateCosineSimilarity($vectorA, $vectorB)
|
||||
{
|
||||
$dotProduct = 0;
|
||||
$magnitudeA = 0;
|
||||
$magnitudeB = 0;
|
||||
|
||||
foreach ($vectorA as $key => $valueA) {
|
||||
$valueB = $vectorB[$key] ?? 0;
|
||||
|
||||
$dotProduct += $valueA * $valueB;
|
||||
$magnitudeA += $valueA * $valueA;
|
||||
$magnitudeB += $valueB * $valueB;
|
||||
}
|
||||
|
||||
$magnitudeA = sqrt($magnitudeA);
|
||||
$magnitudeB = sqrt($magnitudeB);
|
||||
|
||||
if ($magnitudeA == 0 || $magnitudeB == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $dotProduct / ($magnitudeA * $magnitudeB);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get dashboard data including nearby and recommended tailors
|
||||
*/
|
||||
public function getDashboardData(Request $request)
|
||||
{
|
||||
try {
|
||||
$user = Auth::user();//Periksa apakah user yang login adalah pelanggan?
|
||||
if (!$user || !$user->isPelanggan()) {
|
||||
return $this->sendError('Unauthorized.', [], 401);
|
||||
}
|
||||
|
||||
// Get nearby tailors- Mendapatkan Penjahit terdekat
|
||||
$nearbyTailors = User::where('role', 'penjahit')
|
||||
->whereNotNull('latitude')
|
||||
->whereNotNull('longitude')
|
||||
->with(['specializations', 'services'])
|
||||
->get()
|
||||
->map(function ($tailor) use ($user) {
|
||||
if ($user->latitude && $user->longitude && $tailor->latitude && $tailor->longitude) {
|
||||
$tailor->distance = $this->calculateDistance(
|
||||
$user->latitude,
|
||||
$user->longitude,
|
||||
$tailor->latitude,
|
||||
$tailor->longitude
|
||||
);
|
||||
} else {
|
||||
$tailor->distance = null;
|
||||
}
|
||||
return $tailor;
|
||||
})
|
||||
->sortBy('distance')
|
||||
->take(5);
|
||||
|
||||
// Get recommended tailors based on user's preferences
|
||||
$preferredSpecIds = $user->preferredSpecializations()
|
||||
->select('tailor_specializations.id')
|
||||
->pluck('tailor_specializations.id')
|
||||
->toArray();
|
||||
|
||||
$recommendedTailors = collect([]);
|
||||
if (!empty($preferredSpecIds)) {
|
||||
$recommendedTailors = User::where('role', 'penjahit')
|
||||
->with(['specializations', 'services'])
|
||||
->whereHas('specializations', function ($query) use ($preferredSpecIds) {
|
||||
$query->whereIn('tailor_specializations.id', $preferredSpecIds);
|
||||
})
|
||||
->withCount(['bookings' => function ($query) {
|
||||
$query->where('status', 'selesai');
|
||||
}])
|
||||
->orderBy('bookings_count', 'desc')
|
||||
->take(5)
|
||||
->get()
|
||||
->map(function ($tailor) use ($user) {
|
||||
if ($user->latitude && $user->longitude && $tailor->latitude && $tailor->longitude) {
|
||||
$tailor->distance = $this->calculateDistance(
|
||||
$user->latitude,
|
||||
$user->longitude,
|
||||
$tailor->latitude,
|
||||
$tailor->longitude
|
||||
);
|
||||
} else {
|
||||
$tailor->distance = null;
|
||||
}
|
||||
return $tailor;
|
||||
});
|
||||
}
|
||||
|
||||
return $this->sendResponse([
|
||||
'nearby_tailors' => $nearbyTailors->values(),
|
||||
'recommended_tailors' => $recommendedTailors->values(),
|
||||
'user_preferences' => $user->preferredSpecializations
|
||||
], 'Dashboard data retrieved successfully.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Dashboard error: ' . $e->getMessage());
|
||||
return $this->sendError('Error retrieving dashboard data.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance between two points in kilometers
|
||||
* Fungsi untuk perhitungan Jarak untuk mencari penjahit Terdekat
|
||||
*/
|
||||
private function calculateDistance($lat1, $lon1, $lat2, $lon2)
|
||||
{
|
||||
$earthRadius = 6371; // Jari-jari bumi dalam kilometer
|
||||
|
||||
$latDelta = deg2rad($lat2 - $lat1);
|
||||
$lonDelta = deg2rad($lon2 - $lon1);
|
||||
|
||||
$a = sin($latDelta / 2) * sin($latDelta / 2) +
|
||||
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
|
||||
sin($lonDelta / 2) * sin($lonDelta / 2);
|
||||
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
|
||||
return round($earthRadius * $c, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed information about a tailor
|
||||
*/
|
||||
public function getTailorDetail($id)
|
||||
{
|
||||
try {
|
||||
// Log untuk debug
|
||||
\Log::info('Accessing getTailorDetail method', ['id' => $id]);
|
||||
|
||||
$tailor = User::where('id', $id)
|
||||
->where('role', 'penjahit')
|
||||
->with([
|
||||
'specializations',
|
||||
'services' => function($query) {
|
||||
$query->where('is_available', true);
|
||||
},
|
||||
'gallery'
|
||||
])
|
||||
->withCount([
|
||||
'bookings as completed_orders' => function($query) {
|
||||
$query->where('status', 'selesai');
|
||||
}
|
||||
])
|
||||
->first();
|
||||
|
||||
\Log::info('Tailor query result:', ['tailor' => $tailor ? 'found' : 'not found']);
|
||||
|
||||
if (!$tailor) {
|
||||
return $this->sendError('Not found.', ['error' => 'Penjahit tidak ditemukan'], 404);
|
||||
}
|
||||
|
||||
// Get average rating
|
||||
$avgRating = DB::table('tailor_ratings')
|
||||
->where('tailor_id', $id)
|
||||
->avg('rating');
|
||||
|
||||
$tailor->average_rating = round($avgRating ?? 0, 1);
|
||||
|
||||
// Calculate distance if user is logged in and has coordinates
|
||||
$user = Auth::user();
|
||||
if ($user && $user->role === 'pelanggan' && $user->latitude && $user->longitude && $tailor->latitude && $tailor->longitude) {
|
||||
$tailor->distance = $this->calculateDistance(
|
||||
$user->latitude,
|
||||
$user->longitude,
|
||||
$tailor->latitude,
|
||||
$tailor->longitude
|
||||
);
|
||||
} else {
|
||||
$tailor->distance = null;
|
||||
}
|
||||
|
||||
// Get gallery photos
|
||||
$gallery = \App\Models\TailorGallery::where('user_id', $id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
// Prepare response data
|
||||
$responseData = $tailor->toArray();
|
||||
$responseData['gallery'] = $gallery;
|
||||
|
||||
return $this->sendResponse($responseData, 'Detail penjahit berhasil diambil');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Error in getTailorDetail', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil detail penjahit: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search tailors by name
|
||||
*
|
||||
* @param string $name
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function searchByName($name)
|
||||
{
|
||||
try {
|
||||
if (!$name) {
|
||||
return $this->sendError('Error.', ['error' => 'Parameter nama diperlukan'], 400);
|
||||
}
|
||||
|
||||
$query = User::where('role', 'penjahit')
|
||||
->where('name', 'LIKE', "%{$name}%")
|
||||
->with('specializations');
|
||||
|
||||
// Get tailors
|
||||
$tailors = $query->get();
|
||||
|
||||
// Add distance if customer is logged in and has coordinates
|
||||
$user = Auth::user();
|
||||
if ($user && $user->role === 'pelanggan' && $user->latitude && $user->longitude) {
|
||||
foreach ($tailors as $tailor) {
|
||||
if ($tailor->latitude && $tailor->longitude) {
|
||||
$tailor->distance = $this->calculateDistance(
|
||||
$user->latitude,
|
||||
$user->longitude,
|
||||
$tailor->latitude,
|
||||
$tailor->longitude
|
||||
);
|
||||
} else {
|
||||
$tailor->distance = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by distance if available
|
||||
$tailors = $tailors->sortBy('distance');
|
||||
}
|
||||
|
||||
return $this->sendResponse([
|
||||
'tailors' => $tailors->values()
|
||||
], 'Data penjahit berhasil diambil');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mencari penjahit: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\TailorService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class TailorServiceController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Get all services for the authenticated tailor
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
$services = TailorService::where('user_id', Auth::id())
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return $this->sendResponse($services, 'Data jasa berhasil diambil');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil data jasa'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new service
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'required|string|max:1000',
|
||||
'price' => 'required|numeric|min:0',
|
||||
'category' => 'required|string|in:Bawahan,Atasan,Terusan,Perbaikan',
|
||||
'estimated_days' => 'required|integer|min:1',
|
||||
'service_photo' => 'nullable|image|mimes:jpeg,png,jpg|max:2048'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
$serviceData = $request->only([
|
||||
'name',
|
||||
'description',
|
||||
'price',
|
||||
'category',
|
||||
'estimated_days'
|
||||
]);
|
||||
|
||||
$serviceData['user_id'] = Auth::id();
|
||||
|
||||
// Handle service photo upload
|
||||
if ($request->hasFile('service_photo')) {
|
||||
$path = $request->file('service_photo')->store('service_photos', 'public');
|
||||
$serviceData['service_photo'] = $path;
|
||||
}
|
||||
|
||||
$service = TailorService::create($serviceData);
|
||||
|
||||
return $this->sendResponse($service, 'Jasa berhasil ditambahkan');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat menambahkan jasa'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified service
|
||||
*/
|
||||
public function update(Request $request, TailorService $service)
|
||||
{
|
||||
try {
|
||||
// Check if the service belongs to the authenticated tailor
|
||||
if ($service->user_id !== Auth::id()) {
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk mengubah jasa ini'], 403);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'required|string|max:1000',
|
||||
'price' => 'required|numeric|min:0',
|
||||
'category' => 'required|string|in:Bawahan,Atasan,Terusan,Perbaikan',
|
||||
'estimated_days' => 'required|integer|min:1',
|
||||
'is_available' => 'required|boolean',
|
||||
'service_photo' => 'nullable|image|mimes:jpeg,png,jpg|max:2048'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Error validasi.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
// Handle service photo upload
|
||||
if ($request->hasFile('service_photo')) {
|
||||
// Delete old photo if exists
|
||||
if ($service->service_photo) {
|
||||
Storage::disk('public')->delete($service->service_photo);
|
||||
}
|
||||
|
||||
$path = $request->file('service_photo')->store('service_photos', 'public');
|
||||
$service->service_photo = $path;
|
||||
}
|
||||
|
||||
$service->update($request->only([
|
||||
'name',
|
||||
'description',
|
||||
'price',
|
||||
'category',
|
||||
'estimated_days',
|
||||
'is_available'
|
||||
]));
|
||||
|
||||
return $this->sendResponse($service, 'Jasa berhasil diupdate');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengupdate jasa'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the specified service
|
||||
*/
|
||||
public function destroy(TailorService $service)
|
||||
{
|
||||
try {
|
||||
// Check if the service belongs to the authenticated tailor
|
||||
if ($service->user_id !== Auth::id()) {
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk menghapus jasa ini'], 403);
|
||||
}
|
||||
|
||||
// Delete service photo if exists
|
||||
if ($service->service_photo) {
|
||||
Storage::disk('public')->delete($service->service_photo);
|
||||
}
|
||||
|
||||
$service->delete();
|
||||
|
||||
return $this->sendResponse(null, 'Jasa berhasil dihapus');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat menghapus jasa'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle service availability
|
||||
*/
|
||||
public function toggleAvailability(TailorService $service)
|
||||
{
|
||||
try {
|
||||
// Check if the service belongs to the authenticated tailor
|
||||
if ($service->user_id !== Auth::id()) {
|
||||
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk mengubah status jasa ini'], 403);
|
||||
}
|
||||
|
||||
$service->is_available = !$service->is_available;
|
||||
$service->save();
|
||||
|
||||
return $this->sendResponse($service, 'Status ketersediaan jasa berhasil diubah');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengubah status jasa'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get services by tailor ID (public)
|
||||
*/
|
||||
public function getTailorServices($tailorId)
|
||||
{
|
||||
try {
|
||||
$services = TailorService::where('user_id', $tailorId)
|
||||
->where('is_available', true)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return $this->sendResponse($services, 'Data jasa penjahit berhasil diambil');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil data jasa'], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,416 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\TailorSpecialization;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class TailorSpecializationController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
$specializations = TailorSpecialization::all();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $specializations
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Gagal mengambil data spesialisasi',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'category' => 'required|string|max:255',
|
||||
'icon' => 'nullable|string',
|
||||
'photo' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
\Log::error('Validation failed', ['errors' => $validator->errors()]);
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
$specialization = new TailorSpecialization();
|
||||
$specialization->name = $request->name;
|
||||
$specialization->category = $request->category;
|
||||
$specialization->icon = $request->icon;
|
||||
$specialization->save();
|
||||
|
||||
if ($request->hasFile('photo')) {
|
||||
try {
|
||||
$photo = $request->file('photo');
|
||||
|
||||
\Log::info('Photo file information', [
|
||||
'original_name' => $photo->getClientOriginalName(),
|
||||
'mime_type' => $photo->getMimeType(),
|
||||
'size' => $photo->getSize(),
|
||||
'error' => $photo->getError()
|
||||
]);
|
||||
|
||||
if (!$photo->isValid()) {
|
||||
throw new \Exception('Invalid file upload: ' . $photo->getErrorMessage());
|
||||
}
|
||||
|
||||
// Simpan file menggunakan putFileAs
|
||||
$filename = 'specialization_' . time() . '_' . $specialization->id . '.' . $photo->getClientOriginalExtension();
|
||||
|
||||
// Pastikan direktori exists
|
||||
$path = storage_path('app/public/specialization_photos');
|
||||
if (!file_exists($path)) {
|
||||
mkdir($path, 0755, true);
|
||||
}
|
||||
|
||||
// Simpan file menggunakan move
|
||||
$photo->move($path, $filename);
|
||||
|
||||
// Verifikasi file exists
|
||||
if (!file_exists($path . '/' . $filename)) {
|
||||
throw new \Exception('File not found after moving');
|
||||
}
|
||||
|
||||
$specialization->photo = '/storage/specialization_photos/' . $filename;
|
||||
$specialization->save();
|
||||
|
||||
\Log::info('File saved successfully', [
|
||||
'path' => $path . '/' . $filename,
|
||||
'exists' => file_exists($path . '/' . $filename),
|
||||
'url' => $specialization->photo
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Error handling photo upload', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
throw new \Exception('Error handling photo: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Spesialisasi berhasil ditambahkan',
|
||||
'data' => $specialization
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Error in store method', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Terjadi kesalahan saat menambahkan spesialisasi: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
try {
|
||||
$specialization = TailorSpecialization::findOrFail($id);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $specialization
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Spesialisasi tidak ditemukan',
|
||||
'error' => $e->getMessage()
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
\Log::info('Update request received', [
|
||||
'id' => $id,
|
||||
'request_data' => $request->all(),
|
||||
'has_file' => $request->hasFile('photo'),
|
||||
'content_type' => $request->header('Content-Type'),
|
||||
'files' => $_FILES, // Log PHP's native FILES array
|
||||
'method' => $request->method()
|
||||
]);
|
||||
|
||||
// Alternate way to get form data for PUT requests
|
||||
$input = [];
|
||||
if ($request->isMethod('put') && empty($request->all())) {
|
||||
// Try to get data from PHP's input directly for PUT
|
||||
parse_str(file_get_contents("php://input"), $putData);
|
||||
\Log::info('PUT data parsed', ['put_data' => $putData]);
|
||||
|
||||
// Basic fields
|
||||
$input['name'] = $putData['name'] ?? null;
|
||||
$input['category'] = $putData['category'] ?? null;
|
||||
$input['icon'] = $putData['icon'] ?? null;
|
||||
|
||||
// Use native FILES for the photo
|
||||
if (!empty($_FILES['photo']['name'])) {
|
||||
$input['photo'] = $_FILES['photo'];
|
||||
}
|
||||
} else {
|
||||
$input = $request->all();
|
||||
}
|
||||
|
||||
// Debug untuk memastikan data diterima dengan benar
|
||||
\Log::info('Input data', [
|
||||
'input' => $input,
|
||||
'name_exists' => isset($input['name']),
|
||||
'category_exists' => isset($input['category']),
|
||||
]);
|
||||
|
||||
$validator = Validator::make($input, [
|
||||
'name' => 'required|string|max:255',
|
||||
'category' => 'required|string|max:255',
|
||||
'icon' => 'nullable|string',
|
||||
'photo' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
\Log::error('Validation failed', [
|
||||
'errors' => $validator->errors()
|
||||
]);
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Validasi gagal',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
$specialization = TailorSpecialization::findOrFail($id);
|
||||
\Log::info('Current specialization data', [
|
||||
'before_update' => $specialization->toArray()
|
||||
]);
|
||||
|
||||
// Update basic fields
|
||||
$specialization->name = $input['name'];
|
||||
$specialization->category = $input['category'];
|
||||
$specialization->icon = $input['icon'] ?? null;
|
||||
|
||||
if ($request->hasFile('photo')) {
|
||||
\Log::info('Processing photo upload');
|
||||
// Delete old photo if exists
|
||||
if ($specialization->photo) {
|
||||
$oldPhotoPath = str_replace('/storage/', '', $specialization->photo);
|
||||
if (Storage::disk('public')->exists($oldPhotoPath)) {
|
||||
Storage::disk('public')->delete($oldPhotoPath);
|
||||
\Log::info('Old photo deleted', ['path' => $oldPhotoPath]);
|
||||
}
|
||||
}
|
||||
|
||||
$photo = $request->file('photo');
|
||||
$filename = 'specialization_' . time() . '_' . $id . '.' . $photo->getClientOriginalExtension();
|
||||
|
||||
// Pastikan direktori exists
|
||||
$path = storage_path('app/public/specialization_photos');
|
||||
if (!file_exists($path)) {
|
||||
mkdir($path, 0755, true);
|
||||
}
|
||||
|
||||
// Simpan file menggunakan move
|
||||
$photo->move($path, $filename);
|
||||
|
||||
$specialization->photo = '/storage/specialization_photos/' . $filename;
|
||||
\Log::info('New photo saved', ['path' => $specialization->photo]);
|
||||
}
|
||||
|
||||
$specialization->save();
|
||||
|
||||
// Force refresh from database
|
||||
$specialization = TailorSpecialization::findOrFail($id);
|
||||
|
||||
\Log::info('Specialization updated successfully', [
|
||||
'after_update' => $specialization->toArray()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Spesialisasi berhasil diperbarui',
|
||||
'data' => $specialization
|
||||
]);
|
||||
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
\Log::error('Specialization not found', ['id' => $id]);
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Spesialisasi tidak ditemukan'
|
||||
], 404);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Update failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Gagal memperbarui spesialisasi',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
try {
|
||||
$specialization = TailorSpecialization::findOrFail($id);
|
||||
|
||||
// Hapus foto jika ada
|
||||
if ($specialization->photo) {
|
||||
$photoPath = str_replace('/storage/', '', $specialization->photo);
|
||||
if (Storage::disk('public')->exists($photoPath)) {
|
||||
Storage::disk('public')->delete($photoPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Hapus spesialisasi
|
||||
$specialization->delete();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Spesialisasi berhasil dihapus'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Gagal menghapus spesialisasi',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all specializations with photos grouped by category
|
||||
*/
|
||||
public function getAllSpecializations()
|
||||
{
|
||||
try {
|
||||
$specializations = TailorSpecialization::all()
|
||||
->groupBy('category')
|
||||
->map(function ($items) {
|
||||
return $items->map(function ($item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'name' => $item->name,
|
||||
'photo' => $item->photo,
|
||||
'category' => $item->category
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Data spesialisasi berhasil diambil',
|
||||
'data' => $specializations
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Terjadi kesalahan saat mengambil data spesialisasi',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update foto spesialisasi
|
||||
*/
|
||||
public function updatePhoto(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
\Log::info('User attempting to update photo', [
|
||||
'user' => auth()->user(),
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
if (!auth()->user()->isAdmin()) {
|
||||
\Log::warning('Unauthorized access attempt', [
|
||||
'user' => auth()->user(),
|
||||
'role' => auth()->user()->role
|
||||
]);
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Unauthorized',
|
||||
'error' => 'Only admin can update specialization photos'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'photo' => 'required|image|mimes:jpeg,png,jpg|max:2048'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
\Log::error('Validation failed', [
|
||||
'errors' => $validator->errors()
|
||||
]);
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Error validasi',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
$specialization = TailorSpecialization::findOrFail($id);
|
||||
|
||||
// Hapus foto lama jika ada
|
||||
if ($specialization->photo) {
|
||||
$oldPhotoPath = str_replace('/storage/', '', $specialization->photo);
|
||||
if (Storage::disk('public')->exists($oldPhotoPath)) {
|
||||
Storage::disk('public')->delete($oldPhotoPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Upload dan simpan foto baru
|
||||
$photo = $request->file('photo');
|
||||
$fileName = 'specialization_' . time() . '_' . $id . '.' . $photo->getClientOriginalExtension();
|
||||
$photo->storeAs('specialization_photos', $fileName, 'public');
|
||||
|
||||
// Update path foto di database
|
||||
$specialization->photo = '/storage/specialization_photos/' . $fileName;
|
||||
$specialization->save();
|
||||
|
||||
\Log::info('Photo updated successfully', [
|
||||
'specialization_id' => $id,
|
||||
'file_name' => $fileName
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Foto spesialisasi berhasil diupdate',
|
||||
'data' => [
|
||||
'photo' => $specialization->photo
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Error updating photo', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Error update foto',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Wallet;
|
||||
use App\Models\BankAccount;
|
||||
use App\Models\Withdrawal;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class WalletController extends BaseController
|
||||
{
|
||||
public function getWallet()
|
||||
{
|
||||
try {
|
||||
$wallet = Auth::user()->wallet;
|
||||
if (!$wallet) {
|
||||
$wallet = Wallet::create([
|
||||
'user_id' => Auth::id(),
|
||||
'balance' => 0
|
||||
]);
|
||||
}
|
||||
|
||||
// Menghitung total pending withdrawals
|
||||
$pendingWithdrawals = Withdrawal::where('wallet_id', $wallet->id)
|
||||
->whereIn('status', ['pending', 'processing'])
|
||||
->sum('amount');
|
||||
|
||||
// Menghitung saldo yang tersedia
|
||||
$availableBalance = $wallet->balance - $pendingWithdrawals;
|
||||
|
||||
return $this->sendResponse([
|
||||
'balance' => $wallet->balance,
|
||||
'pending_withdrawals' => $pendingWithdrawals,
|
||||
'available_balance' => $availableBalance,
|
||||
'transactions' => $wallet->transactions()->with('booking')->latest()->get()
|
||||
], 'Wallet information retrieved successfully');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function registerBankAccount(Request $request)
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'bank_name' => 'required|string',
|
||||
'account_number' => 'required|string',
|
||||
'account_holder_name' => 'required|string'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Validation Error.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
$bankAccount = BankAccount::create([
|
||||
'user_id' => Auth::id(),
|
||||
'bank_name' => $request->bank_name,
|
||||
'account_number' => $request->account_number,
|
||||
'account_holder_name' => $request->account_holder_name,
|
||||
'status' => 'pending'
|
||||
]);
|
||||
|
||||
return $this->sendResponse($bankAccount, 'Bank account registered successfully');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function requestWithdrawal(Request $request)
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'bank_account_id' => 'required|exists:bank_accounts,id',
|
||||
'amount' => 'required|numeric|min:10000'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendError('Validation Error.', $validator->errors(), 422);
|
||||
}
|
||||
|
||||
$wallet = Auth::user()->wallet;
|
||||
if (!$wallet) {
|
||||
return $this->sendError('Wallet not found.', [], 404);
|
||||
}
|
||||
|
||||
// Validasi saldo yang tersedia
|
||||
if ($wallet->balance < $request->amount) {
|
||||
return $this->sendError('Insufficient balance.', [
|
||||
'balance' => $wallet->balance,
|
||||
'requested_amount' => $request->amount
|
||||
], 422);
|
||||
}
|
||||
|
||||
$bankAccount = BankAccount::find($request->bank_account_id);
|
||||
if ($bankAccount->status !== 'active') {
|
||||
return $this->sendError('Invalid bank account.', [], 422);
|
||||
}
|
||||
|
||||
// Kurangi saldo wallet
|
||||
$wallet->deductBalance(
|
||||
$request->amount,
|
||||
'Withdrawal request #' . time()
|
||||
);
|
||||
|
||||
$withdrawal = Withdrawal::create([
|
||||
'wallet_id' => $wallet->id,
|
||||
'bank_account_id' => $bankAccount->id,
|
||||
'amount' => $request->amount,
|
||||
'status' => 'pending'
|
||||
]);
|
||||
|
||||
return $this->sendResponse([
|
||||
'withdrawal' => $withdrawal,
|
||||
'wallet' => [
|
||||
'balance' => $wallet->balance,
|
||||
'pending_withdrawals' => $request->amount,
|
||||
'available_balance' => $wallet->balance
|
||||
]
|
||||
], 'Withdrawal request submitted successfully');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function getWithdrawalHistory(Request $request)
|
||||
{
|
||||
try {
|
||||
$wallet = Auth::user()->wallet;
|
||||
if (!$wallet) {
|
||||
return $this->sendResponse([], 'No withdrawal history found');
|
||||
}
|
||||
|
||||
$query = $wallet->withdrawals()->with('bankAccount');
|
||||
|
||||
// Filter berdasarkan status jika parameter status ada
|
||||
if ($request->has('status') && in_array($request->status, ['pending', 'processing', 'completed', 'rejected'])) {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
$withdrawals = $query->latest()->get();
|
||||
|
||||
return $this->sendResponse($withdrawals, 'Withdrawal history retrieved successfully');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function getBankAccounts()
|
||||
{
|
||||
try {
|
||||
$bankAccounts = Auth::user()->bankAccounts()
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return $this->sendResponse($bankAccounts, 'Bank accounts retrieved successfully');
|
||||
} catch (\Exception $e) {
|
||||
return $this->sendError('Error.', ['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class FileAccessController extends Controller
|
||||
{
|
||||
/**
|
||||
* Menampilkan file dari storage
|
||||
*
|
||||
* @param string $path
|
||||
* @return BinaryFileResponse
|
||||
*/
|
||||
public function serveFile($path)
|
||||
{
|
||||
// Decode URL path untuk menangani nama file yang mengandung karakter khusus
|
||||
$path = urldecode($path);
|
||||
|
||||
// Cek apakah file ada di storage public
|
||||
if (!Storage::disk('public')->exists($path)) {
|
||||
abort(404, 'File tidak ditemukan');
|
||||
}
|
||||
|
||||
// Dapatkan path fisik file
|
||||
$filePath = Storage::disk('public')->path($path);
|
||||
|
||||
// Set tipe konten berdasarkan ekstensi file
|
||||
$contentType = $this->getContentType($filePath);
|
||||
|
||||
// Pastikan file bisa dibaca
|
||||
if (!File::isReadable($filePath)) {
|
||||
abort(403, 'Tidak dapat membaca file');
|
||||
}
|
||||
|
||||
// Return file response dengan header yang tepat
|
||||
return response()->file($filePath, [
|
||||
'Content-Type' => $contentType,
|
||||
'Cache-Control' => 'public, max-age=86400',
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'X-Content-Type-Options' => 'nosniff'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Menampilkan file dari subdirektori tertentu
|
||||
*
|
||||
* @param string $directory
|
||||
* @param string $filename
|
||||
* @return BinaryFileResponse
|
||||
*/
|
||||
public function serveDirectoryFile($directory, $filename)
|
||||
{
|
||||
return $this->serveFile("$directory/$filename");
|
||||
}
|
||||
|
||||
/**
|
||||
* Mendapatkan tipe konten berdasarkan ekstensi file
|
||||
*
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
*/
|
||||
private function getContentType($filePath)
|
||||
{
|
||||
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
|
||||
|
||||
$contentTypes = [
|
||||
'png' => 'image/png',
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'gif' => 'image/gif',
|
||||
'svg' => 'image/svg+xml',
|
||||
'pdf' => 'application/pdf',
|
||||
'doc' => 'application/msword',
|
||||
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'xls' => 'application/vnd.ms-excel',
|
||||
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'zip' => 'application/zip',
|
||||
'mp4' => 'video/mp4',
|
||||
'mp3' => 'audio/mpeg',
|
||||
];
|
||||
|
||||
return $contentTypes[strtolower($extension)] ?? 'application/octet-stream';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
/**
|
||||
* The application's route middleware.
|
||||
*
|
||||
* These middleware may be assigned to groups or used individually.
|
||||
*
|
||||
* @var array<string, class-string|string>
|
||||
*/
|
||||
protected $middleware = [
|
||||
//
|
||||
\App\Http\Middleware\CorsMiddleware::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*
|
||||
* @var array<string, array<int, class-string|string>>
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
// Web middleware group
|
||||
],
|
||||
|
||||
'api' => [
|
||||
// API middleware group
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware aliases.
|
||||
*
|
||||
* Aliases may be used instead of class names to assign middleware to routes and groups.
|
||||
*
|
||||
* @var array<string, class-string|string>
|
||||
*/
|
||||
protected $middlewareAliases = [
|
||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
||||
'signed' => \App\Http\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
'role' => \App\Http\Middleware\CheckRole::class,
|
||||
'cors' => \App\Http\Middleware\CorsMiddleware::class,
|
||||
];
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CheckRole
|
||||
{
|
||||
public function handle(Request $request, Closure $next, ...$roles)
|
||||
{
|
||||
if (!$request->user()) {
|
||||
return response()->json(['error' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
// Admin can access everything
|
||||
if ($request->user()->isAdmin()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// For non-admin users, check if they have the required role
|
||||
if (!in_array($request->user()->role, $roles)) {
|
||||
return response()->json(['error' => 'Forbidden. You do not have the required role.'], 403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CorsMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
// Prekonfigurasi CORS untuk OPTIONS request (preflight)
|
||||
if ($request->isMethod('OPTIONS')) {
|
||||
$response = response('', 200);
|
||||
} else {
|
||||
// Melanjutkan request
|
||||
$response = $next($request);
|
||||
}
|
||||
|
||||
// Menambahkan header CORS
|
||||
if ($response instanceof Response) {
|
||||
$response->headers->set('Access-Control-Allow-Origin', '*');
|
||||
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-XSRF-TOKEN, X-CSRF-TOKEN, Accept');
|
||||
$response->headers->set('Access-Control-Max-Age', '86400');
|
||||
$response->headers->set('Access-Control-Expose-Headers', 'Content-Disposition');
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BankAccount extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'bank_name',
|
||||
'account_number',
|
||||
'account_holder_name',
|
||||
'status',
|
||||
'rejection_reason',
|
||||
'verified_at'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'verified_at' => 'datetime'
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function withdrawals()
|
||||
{
|
||||
return $this->hasMany(Withdrawal::class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\TailorRating;
|
||||
|
||||
class Booking extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'customer_id',
|
||||
'tailor_id',
|
||||
'transaction_code',
|
||||
'appointment_date',
|
||||
'appointment_time',
|
||||
'service_type',
|
||||
'category',
|
||||
'design_photo',
|
||||
'notes',
|
||||
'status',
|
||||
'total_price',
|
||||
'completion_date',
|
||||
'payment_status',
|
||||
'payment_method',
|
||||
'midtrans_snap_token',
|
||||
'measurements',
|
||||
'repair_details',
|
||||
'repair_photo',
|
||||
'repair_notes',
|
||||
'completion_photo',
|
||||
'completion_notes',
|
||||
'accepted_at',
|
||||
'rejected_at',
|
||||
'completed_at',
|
||||
'rejection_reason',
|
||||
'pickup_date'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'customer_id' => 'integer',
|
||||
'tailor_id' => 'integer',
|
||||
'total_price' => 'decimal:2',
|
||||
'appointment_date' => 'date',
|
||||
'appointment_time' => 'datetime:H:i',
|
||||
'completion_date' => 'date',
|
||||
'measurements' => 'array',
|
||||
'repair_details' => 'array',
|
||||
'accepted_at' => 'datetime',
|
||||
'rejected_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
'pickup_date' => 'date'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the customer that owns the booking.
|
||||
*/
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'customer_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tailor that owns the booking.
|
||||
*/
|
||||
public function tailor()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'tailor_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ratings for the booking.
|
||||
*/
|
||||
public function ratings()
|
||||
{
|
||||
return $this->hasMany(TailorRating::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if booking can be cancelled
|
||||
*/
|
||||
public function canBeCancelled()
|
||||
{
|
||||
return $this->status === 'reservasi';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if booking can be processed
|
||||
*/
|
||||
public function canBeProcessed()
|
||||
{
|
||||
return $this->status === 'reservasi' && $this->payment_status === 'paid';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if booking can be marked as completed
|
||||
*/
|
||||
public function canBeCompleted()
|
||||
{
|
||||
return $this->status === 'diproses';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TailorGallery extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'photo',
|
||||
'title',
|
||||
'description',
|
||||
'category'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the tailor that owns the gallery item
|
||||
*/
|
||||
public function tailor()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TailorRating extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'booking_id',
|
||||
'customer_id',
|
||||
'tailor_id',
|
||||
'rating',
|
||||
'review'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'booking_id' => 'integer',
|
||||
'customer_id' => 'integer',
|
||||
'tailor_id' => 'integer',
|
||||
'rating' => 'decimal:1'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the booking that owns the rating.
|
||||
*/
|
||||
public function booking()
|
||||
{
|
||||
return $this->belongsTo(Booking::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customer who gave the rating.
|
||||
*/
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'customer_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tailor who received the rating.
|
||||
*/
|
||||
public function tailor()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'tailor_id');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TailorService extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'name',
|
||||
'description',
|
||||
'price',
|
||||
'category',
|
||||
'estimated_days',
|
||||
'is_available',
|
||||
'service_photo'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'price' => 'decimal:2',
|
||||
'estimated_days' => 'integer',
|
||||
'is_available' => 'boolean'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the tailor that owns the service
|
||||
*/
|
||||
public function tailor()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TailorSpecialization extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'icon',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer'
|
||||
];
|
||||
|
||||
/**
|
||||
* The tailors that belong to the specialization.
|
||||
*/
|
||||
public function tailors()
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'tailor_specialization_user')
|
||||
->where('role', 'penjahit');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use App\Notifications\ResetPasswordNotification;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'role',
|
||||
'phone_number',
|
||||
'address',
|
||||
'shop_description',
|
||||
'profile_photo',
|
||||
'latitude',
|
||||
'longitude',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'id' => 'integer',
|
||||
'latitude' => 'float',
|
||||
'longitude' => 'float',
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if user is admin
|
||||
*/
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->role === 'admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is penjahit
|
||||
*/
|
||||
public function isPenjahit(): bool
|
||||
{
|
||||
return $this->role === 'penjahit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is pelanggan
|
||||
*/
|
||||
public function isPelanggan(): bool
|
||||
{
|
||||
return $this->role === 'pelanggan';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has specific role
|
||||
*/
|
||||
public function hasRole(string $role): bool
|
||||
{
|
||||
return $this->role === $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the specializations for the tailor.
|
||||
*/
|
||||
public function specializations()
|
||||
{
|
||||
return $this->belongsToMany(TailorSpecialization::class, 'tailor_specialization_user');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preferred specializations for the customer.
|
||||
*/
|
||||
public function preferredSpecializations()
|
||||
{
|
||||
return $this->belongsToMany(TailorSpecialization::class, 'customer_specialization');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services for the tailor.
|
||||
*/
|
||||
public function services()
|
||||
{
|
||||
return $this->hasMany(TailorService::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bookings for the user (as customer).
|
||||
*/
|
||||
public function customerBookings()
|
||||
{
|
||||
return $this->hasMany(Booking::class, 'customer_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bookings for the user (as tailor).
|
||||
*/
|
||||
public function bookings()
|
||||
{
|
||||
return $this->hasMany(Booking::class, 'tailor_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the gallery items for the tailor.
|
||||
*/
|
||||
public function gallery()
|
||||
{
|
||||
return $this->hasMany(TailorGallery::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ratings for the tailor.
|
||||
*/
|
||||
public function ratings()
|
||||
{
|
||||
return $this->hasMany(TailorRating::class, 'tailor_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ratings given by the customer.
|
||||
*/
|
||||
public function givenRatings()
|
||||
{
|
||||
return $this->hasMany(TailorRating::class, 'customer_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the password reset notification.
|
||||
*
|
||||
* @param string $token
|
||||
* @return void
|
||||
*/
|
||||
public function sendPasswordResetNotification($token)
|
||||
{
|
||||
$this->notify(new ResetPasswordNotification($token));
|
||||
}
|
||||
|
||||
public function wallet()
|
||||
{
|
||||
return $this->hasOne(Wallet::class);
|
||||
}
|
||||
|
||||
public function bankAccounts()
|
||||
{
|
||||
return $this->hasMany(BankAccount::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim notifikasi verifikasi email custom
|
||||
*/
|
||||
public function sendEmailVerificationNotification()
|
||||
{
|
||||
// Generate verification URL yang langsung mengarah ke backend
|
||||
$verificationUrl = url()->temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $this->id, 'hash' => sha1($this->email)]
|
||||
);
|
||||
|
||||
$this->notify(new \App\Notifications\VerifyEmailNotification($verificationUrl));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Wallet extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'balance',
|
||||
'status'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'balance' => 'float'
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function transactions()
|
||||
{
|
||||
return $this->hasMany(WalletTransaction::class);
|
||||
}
|
||||
|
||||
public function withdrawals()
|
||||
{
|
||||
return $this->hasMany(Withdrawal::class);
|
||||
}
|
||||
|
||||
public function addBalance($amount, $description, $booking = null)
|
||||
{
|
||||
$this->balance += $amount;
|
||||
$this->save();
|
||||
|
||||
$this->transactions()->create([
|
||||
'type' => 'credit',
|
||||
'amount' => $amount,
|
||||
'description' => $description,
|
||||
'booking_id' => $booking ? $booking->id : null
|
||||
]);
|
||||
}
|
||||
|
||||
public function deductBalance($amount, $description)
|
||||
{
|
||||
if ($this->balance < $amount) {
|
||||
throw new \Exception('Insufficient balance');
|
||||
}
|
||||
|
||||
$this->balance -= $amount;
|
||||
$this->save();
|
||||
|
||||
$this->transactions()->create([
|
||||
'type' => 'debit',
|
||||
'amount' => $amount,
|
||||
'description' => $description
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WalletTransaction extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'wallet_id',
|
||||
'booking_id',
|
||||
'type',
|
||||
'amount',
|
||||
'description',
|
||||
'status'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'wallet_id' => 'integer',
|
||||
'booking_id' => 'integer',
|
||||
'amount' => 'float'
|
||||
];
|
||||
|
||||
public function wallet()
|
||||
{
|
||||
return $this->belongsTo(Wallet::class);
|
||||
}
|
||||
|
||||
public function booking()
|
||||
{
|
||||
return $this->belongsTo(Booking::class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Withdrawal extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'wallet_id',
|
||||
'bank_account_id',
|
||||
'amount',
|
||||
'status',
|
||||
'rejection_reason',
|
||||
'proof_of_payment',
|
||||
'processed_at'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'wallet_id' => 'integer',
|
||||
'bank_account_id' => 'integer',
|
||||
'amount' => 'float',
|
||||
'processed_at' => 'datetime'
|
||||
];
|
||||
|
||||
public function wallet()
|
||||
{
|
||||
return $this->belongsTo(Wallet::class);
|
||||
}
|
||||
|
||||
public function bankAccount()
|
||||
{
|
||||
return $this->belongsTo(BankAccount::class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class ResetPasswordNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
protected $pin;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct($pin)
|
||||
{
|
||||
$this->pin = $pin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$appName = config('app.name', 'Project Jahit');
|
||||
|
||||
return (new MailMessage)
|
||||
->subject('Reset Password - ' . $appName)
|
||||
->view('emails.reset-password', [
|
||||
'pin' => $this->pin,
|
||||
'name' => $notifiable->name,
|
||||
'appName' => $appName,
|
||||
'expiresIn' => '60 menit'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'pin' => $this->pin,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class VerifyEmailNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
protected $verificationUrl;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct($verificationUrl)
|
||||
{
|
||||
$this->verificationUrl = $verificationUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$appName = config('app.name', 'Project Jahit');
|
||||
|
||||
return (new MailMessage)
|
||||
->subject('Verifikasi Email - ' . $appName)
|
||||
->view('emails.verify-email', [
|
||||
'url' => $this->verificationUrl,
|
||||
'name' => $notifiable->name,
|
||||
'appName' => $appName,
|
||||
'expiresIn' => '60 menit'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'verification_url' => $this->verificationUrl,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
// Jika aplikasi berjalan di produksi, gunakan HTTPS
|
||||
if (config('app.env') === 'production') {
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The model to policy mappings for the application.
|
||||
*
|
||||
* @var array<class-string, class-string>
|
||||
*/
|
||||
protected $policies = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any authentication / authorization services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerPolicies();
|
||||
|
||||
// Define gates for roles
|
||||
Gate::define('admin', function ($user) {
|
||||
return $user->role === 'admin';
|
||||
});
|
||||
|
||||
Gate::define('penjahit', function ($user) {
|
||||
return $user->role === 'penjahit';
|
||||
});
|
||||
|
||||
Gate::define('pelanggan', function ($user) {
|
||||
return $user->role === 'pelanggan';
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The event to listener mappings for the application.
|
||||
*
|
||||
* @var array<class-string, array<int, class-string>>
|
||||
*/
|
||||
protected $listen = [
|
||||
Registered::class => [
|
||||
SendEmailVerificationNotification::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any events for your application.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if events and listeners should be automatically discovered.
|
||||
*/
|
||||
public function shouldDiscoverEvents(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The path to your application's "home" route.
|
||||
*
|
||||
* Typically, users are redirected here after authentication.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const HOME = '/home';
|
||||
|
||||
/**
|
||||
* Define your route model bindings, pattern filters, and other route configuration.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
RateLimiter::for('api', function (Request $request) {
|
||||
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
|
||||
});
|
||||
|
||||
$this->routes(function () {
|
||||
Route::middleware('api')
|
||||
->prefix('api')
|
||||
->group(base_path('routes/api.php'));
|
||||
|
||||
Route::middleware('web')
|
||||
->group(base_path('routes/web.php'));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
|
@ -0,0 +1,578 @@
|
|||
1. PENJAHIT
|
||||
A. REGISTER
|
||||
http://localhost:8000/api/penjahit/register
|
||||
{
|
||||
"name": "Toko Jahit Makmur",
|
||||
"email": "jahit.makmur@example.com",
|
||||
"password": "password123",
|
||||
"phone_number": "081234567891",
|
||||
"address": "Jl. Menjahit Indah No. 123, Jakarta Selatan",
|
||||
"shop_description": "Toko jahit terpercaya dengan pengalaman lebih dari 10 tahun. Menyediakan berbagai jasa jahit dan perbaikan pakaian dengan kualitas premium.",
|
||||
"specializations": [1, 2]
|
||||
}
|
||||
B. Login
|
||||
http://localhost:8000/api/penjahit/login
|
||||
{
|
||||
"email": "jahit.makmurr@example.com",
|
||||
"password": "password"
|
||||
}
|
||||
C. DASHBOARD
|
||||
GET http://localhost:8000/api/penjahit/dashboard
|
||||
INI RESPON
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"incoming_bookings": [],
|
||||
"current_month_earnings": 0,
|
||||
"total_earnings": 0,
|
||||
"average_rating": 0,
|
||||
"total_completed_orders": 0
|
||||
},
|
||||
"message": "Data dashboard berhasil diambil"
|
||||
}
|
||||
|
||||
D.DI FIGMA HALAMAN KONFIRMASI PESANAN
|
||||
GET http://localhost:8000/api/penjahit/bookings/status/pending
|
||||
GET http://localhost:8000/api/penjahit/bookings/status/diterima
|
||||
GET http://localhost:8000/api/penjahit/bookings/status/diproses
|
||||
GET http://localhost:8000/api/penjahit/bookings/status/selesai
|
||||
GET http://localhost:8000/api/penjahit/bookings/status/dibatalkan
|
||||
ALL http://localhost:8000/api/penjahit/bookings
|
||||
|
||||
E. TERIMA DAN TOLAK berdasarkan ia login
|
||||
POST http://localhost:8000/api/bookings/{booking_id}/reject
|
||||
POST http://localhost:8000/api/bookings/{booking_id}/accept
|
||||
|
||||
F. KALENDER GET
|
||||
http://localhost:8000/api/penjahit/calendar/3/2025
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"month": 3,
|
||||
"year": 2025,
|
||||
"month_name": "March",
|
||||
"calendar": [
|
||||
{
|
||||
"date": "2025-03-01",
|
||||
"day": "01",
|
||||
"day_name": "Saturday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-02",
|
||||
"day": "02",
|
||||
"day_name": "Sunday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-03",
|
||||
"day": "03",
|
||||
"day_name": "Monday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-04",
|
||||
"day": "04",
|
||||
"day_name": "Tuesday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-05",
|
||||
"day": "05",
|
||||
"day_name": "Wednesday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-06",
|
||||
"day": "06",
|
||||
"day_name": "Thursday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-07",
|
||||
"day": "07",
|
||||
"day_name": "Friday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-08",
|
||||
"day": "08",
|
||||
"day_name": "Saturday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-09",
|
||||
"day": "09",
|
||||
"day_name": "Sunday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-10",
|
||||
"day": "10",
|
||||
"day_name": "Monday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-11",
|
||||
"day": "11",
|
||||
"day_name": "Tuesday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-12",
|
||||
"day": "12",
|
||||
"day_name": "Wednesday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-13",
|
||||
"day": "13",
|
||||
"day_name": "Thursday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-14",
|
||||
"day": "14",
|
||||
"day_name": "Friday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-15",
|
||||
"day": "15",
|
||||
"day_name": "Saturday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-16",
|
||||
"day": "16",
|
||||
"day_name": "Sunday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-17",
|
||||
"day": "17",
|
||||
"day_name": "Monday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-18",
|
||||
"day": "18",
|
||||
"day_name": "Tuesday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-19",
|
||||
"day": "19",
|
||||
"day_name": "Wednesday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-20",
|
||||
"day": "20",
|
||||
"day_name": "Thursday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-21",
|
||||
"day": "21",
|
||||
"day_name": "Friday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-22",
|
||||
"day": "22",
|
||||
"day_name": "Saturday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-23",
|
||||
"day": "23",
|
||||
"day_name": "Sunday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-24",
|
||||
"day": "24",
|
||||
"day_name": "Monday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-25",
|
||||
"day": "25",
|
||||
"day_name": "Tuesday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-26",
|
||||
"day": "26",
|
||||
"day_name": "Wednesday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-27",
|
||||
"day": "27",
|
||||
"day_name": "Thursday",
|
||||
"bookings": [
|
||||
{
|
||||
"id": 2,
|
||||
"time": "2025-03-26T10:00:00.000000Z",
|
||||
"customer_name": "Rudi Hermawan",
|
||||
"customer_phone": "081234567895",
|
||||
"service_type": "Jahit Baru",
|
||||
"category": "Bawahan",
|
||||
"status": "diproses",
|
||||
"payment_status": "paid"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-03-28",
|
||||
"day": "28",
|
||||
"day_name": "Friday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-29",
|
||||
"day": "29",
|
||||
"day_name": "Saturday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-30",
|
||||
"day": "30",
|
||||
"day_name": "Sunday",
|
||||
"bookings": []
|
||||
},
|
||||
{
|
||||
"date": "2025-03-31",
|
||||
"day": "31",
|
||||
"day_name": "Monday",
|
||||
"bookings": []
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total_bookings": 1,
|
||||
"pending_bookings": 0,
|
||||
"ongoing_bookings": 1,
|
||||
"completed_bookings": 0,
|
||||
"cancelled_bookings": 0
|
||||
}
|
||||
},
|
||||
"message": "Data kalender berhasil diambil"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
2. USERS
|
||||
A. REGISTER
|
||||
http://localhost:8000/api/pelanggan/register
|
||||
{
|
||||
"name": "Pelanggan Baru",
|
||||
"email": "pelanggan.baru22@example.com",
|
||||
"password": "password123",
|
||||
"phone_number": "081234567890",
|
||||
"address": "Jl. Pelanggan No. 123, Jakarta",
|
||||
"preferred_specializations": [3, 5, 9],
|
||||
"latitude": -6.1754,
|
||||
"longitude": 106.8272
|
||||
}
|
||||
B. Login
|
||||
http://localhost:8000/api/pelanggan/login
|
||||
{
|
||||
"email": "pelanggan2@gmail.com",
|
||||
"password": "password"
|
||||
}
|
||||
C. DASHBOARD
|
||||
http://localhost:8000/api/tailors/recommended
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"tailors": [
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Siti Rahayu",
|
||||
"email": "siti@example.com",
|
||||
"role": "penjahit",
|
||||
"phone_number": "081234567892",
|
||||
"address": "Jl. Penjahit No. 2",
|
||||
"latitude": null,
|
||||
"longitude": null,
|
||||
"shop_description": null,
|
||||
"profile_photo": null,
|
||||
"email_verified_at": null,
|
||||
"created_at": "2025-03-24T21:15:14.000000Z",
|
||||
"updated_at": "2025-03-24T21:15:14.000000Z",
|
||||
"distance": null,
|
||||
"specializations": [
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Gamis",
|
||||
"icon": null,
|
||||
"created_at": "2025-03-23T14:12:26.000000Z",
|
||||
"updated_at": "2025-03-23T14:12:26.000000Z",
|
||||
"pivot": {
|
||||
"user_id": 3,
|
||||
"tailor_specialization_id": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Baju Pesta",
|
||||
"icon": "dress",
|
||||
"created_at": "2025-03-24T21:15:13.000000Z",
|
||||
"updated_at": "2025-03-24T21:15:13.000000Z",
|
||||
"pivot": {
|
||||
"user_id": 3,
|
||||
"tailor_specialization_id": 9
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"user_preferred": [
|
||||
3,
|
||||
5,
|
||||
9
|
||||
]
|
||||
},
|
||||
"message": "Rekomendasi penjahit berhasil diambil"
|
||||
}
|
||||
|
||||
D. DETAIL PENJAHIT
|
||||
http://localhost:8000/api/tailors/3
|
||||
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": 3,
|
||||
"name": "Siti Rahayu",
|
||||
"email": "siti@example.com",
|
||||
"role": "penjahit",
|
||||
"phone_number": "081234567892",
|
||||
"address": "Jl. Penjahit No. 2",
|
||||
"latitude": null,
|
||||
"longitude": null,
|
||||
"shop_description": null,
|
||||
"profile_photo": null,
|
||||
"email_verified_at": null,
|
||||
"created_at": "2025-03-24T21:15:14.000000Z",
|
||||
"updated_at": "2025-03-24T21:15:14.000000Z",
|
||||
"completed_orders": 0,
|
||||
"average_rating": 4,
|
||||
"distance": null,
|
||||
"specializations": [
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Gamis",
|
||||
"icon": null,
|
||||
"created_at": "2025-03-23T14:12:26.000000Z",
|
||||
"updated_at": "2025-03-23T14:12:26.000000Z",
|
||||
"pivot": {
|
||||
"user_id": 3,
|
||||
"tailor_specialization_id": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Baju Pesta",
|
||||
"icon": "dress",
|
||||
"created_at": "2025-03-24T21:15:13.000000Z",
|
||||
"updated_at": "2025-03-24T21:15:13.000000Z",
|
||||
"pivot": {
|
||||
"user_id": 3,
|
||||
"tailor_specialization_id": 9
|
||||
}
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"id": 2,
|
||||
"user_id": 3,
|
||||
"name": "Jahit Baju Muslim",
|
||||
"description": "Jahit baju muslim dengan desain modern",
|
||||
"price": "350000.00",
|
||||
"category": "Jahit Baru",
|
||||
"estimated_days": 5,
|
||||
"is_available": true,
|
||||
"service_photo": "services/baju_muslim.jpg",
|
||||
"created_at": "2025-03-24T21:15:15.000000Z",
|
||||
"updated_at": "2025-03-24T21:15:15.000000Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": "Detail penjahit berhasil diambil"
|
||||
}
|
||||
|
||||
E. Melakukan booking
|
||||
http://localhost:8000/api/tailors/3/book
|
||||
INI PERINTAHNYA
|
||||
{
|
||||
"appointment_date": "2025-04-01",
|
||||
"appointment_time": "14:00",
|
||||
"service_type": "Jahit Baru",
|
||||
"category": "Bawahan",
|
||||
"notes": "Celana bahan katun warna hitam",
|
||||
"measurements": [
|
||||
{
|
||||
"name": "lingkar_pinggang",
|
||||
"value": 82,
|
||||
"unit": "cm"
|
||||
},
|
||||
{
|
||||
"name": "lingkar_pinggul",
|
||||
"value": 96,
|
||||
"unit": "cm"
|
||||
},
|
||||
{
|
||||
"name": "panjang_celana",
|
||||
"value": 98,
|
||||
"unit": "cm"
|
||||
},
|
||||
{
|
||||
"name": "lingkar_paha",
|
||||
"value": 58,
|
||||
"unit": "cm"
|
||||
},
|
||||
{
|
||||
"name": "lingkar_lutut",
|
||||
"value": 40,
|
||||
"unit": "cm"
|
||||
},
|
||||
{
|
||||
"name": "lingkar_kaki",
|
||||
"value": 32,
|
||||
"unit": "cm"
|
||||
}
|
||||
]
|
||||
}
|
||||
F. Untuk get semua bookings
|
||||
http://localhost:8000/api/bookings/customer
|
||||
ini responnya
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 5,
|
||||
"customer_id": 13,
|
||||
"tailor_id": 3,
|
||||
"appointment_date": "2025-04-01T00:00:00.000000Z",
|
||||
"appointment_time": "14:00",
|
||||
"service_type": "Jahit Baru",
|
||||
"category": "Bawahan",
|
||||
"design_photo": null,
|
||||
"notes": "Celana bahan katun warna hitam",
|
||||
"status": "reservasi",
|
||||
"total_price": null,
|
||||
"payment_status": "unpaid",
|
||||
"measurements": [
|
||||
{
|
||||
"name": "lingkar_pinggang",
|
||||
"unit": "cm",
|
||||
"value": 82
|
||||
},
|
||||
{
|
||||
"name": "lingkar_pinggul",
|
||||
"unit": "cm",
|
||||
"value": 96
|
||||
},
|
||||
{
|
||||
"name": "panjang_celana",
|
||||
"unit": "cm",
|
||||
"value": 98
|
||||
},
|
||||
{
|
||||
"name": "lingkar_paha",
|
||||
"unit": "cm",
|
||||
"value": 58
|
||||
},
|
||||
{
|
||||
"name": "lingkar_lutut",
|
||||
"unit": "cm",
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"name": "lingkar_kaki",
|
||||
"unit": "cm",
|
||||
"value": 32
|
||||
}
|
||||
],
|
||||
"repair_details": null,
|
||||
"repair_photo": null,
|
||||
"repair_notes": null,
|
||||
"completion_photo": null,
|
||||
"completion_notes": null,
|
||||
"accepted_at": null,
|
||||
"rejected_at": null,
|
||||
"completed_at": null,
|
||||
"rejection_reason": null,
|
||||
"created_at": "2025-03-25T08:10:03.000000Z",
|
||||
"updated_at": "2025-03-25T08:10:03.000000Z",
|
||||
"tailor": {
|
||||
"id": 3,
|
||||
"name": "Siti Rahayu",
|
||||
"email": "siti@example.com",
|
||||
"role": "penjahit",
|
||||
"phone_number": "081234567892",
|
||||
"address": "Jl. Penjahit No. 2",
|
||||
"latitude": null,
|
||||
"longitude": null,
|
||||
"shop_description": null,
|
||||
"profile_photo": null,
|
||||
"email_verified_at": null,
|
||||
"created_at": "2025-03-24T21:15:14.000000Z",
|
||||
"updated_at": "2025-03-24T21:15:14.000000Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"customer_id": 13,
|
||||
"tailor_id": 3,
|
||||
"appointment_date": "2025-04-01T00:00:00.000000Z",
|
||||
"appointment_time": "14:00",
|
||||
"service_type": "Jahit Baru",
|
||||
"category": "Bawahan",
|
||||
"design_photo": null,
|
||||
"notes": "Jahit celana panjang dengan bahan katun",
|
||||
"status": "reservasi",
|
||||
"total_price": null,
|
||||
"payment_status": "unpaid",
|
||||
"measurements": null,
|
||||
"repair_details": null,
|
||||
"repair_photo": null,
|
||||
"repair_notes": null,
|
||||
"completion_photo": null,
|
||||
"completion_notes": null,
|
||||
"accepted_at": null,
|
||||
"rejected_at": null,
|
||||
"completed_at": null,
|
||||
"rejection_reason": null,
|
||||
"created_at": "2025-03-25T07:59:47.000000Z",
|
||||
"updated_at": "2025-03-25T07:59:47.000000Z",
|
||||
"tailor": {
|
||||
"id": 3,
|
||||
"name": "Siti Rahayu",
|
||||
"email": "siti@example.com",
|
||||
"role": "penjahit",
|
||||
"phone_number": "081234567892",
|
||||
"address": "Jl. Penjahit No. 2",
|
||||
"latitude": null,
|
||||
"longitude": null,
|
||||
"shop_description": null,
|
||||
"profile_photo": null,
|
||||
"email_verified_at": null,
|
||||
"created_at": "2025-03-24T21:15:14.000000Z",
|
||||
"updated_at": "2025-03-24T21:15:14.000000Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"message": "Data booking berhasil diambil."
|
||||
}
|
||||
GET BOOKING BERDASRKAN STATUS
|
||||
GET http://localhost:8000/api/bookings/customer/status/reservasi
|
||||
GET http://localhost:8000/api/bookings/customer/status/diproses
|
||||
GET http://localhost:8000/api/bookings/customer/status/selesai
|
||||
GET http://localhost:8000/api/bookings/customer/status/dibatalkan
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
//
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
//
|
||||
})->create();
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
];
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"midtrans/midtrans-php": "^2.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-ide-helper": "^3.5",
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.13",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^11.5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => 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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Frontend URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used to generate links to the frontend application for
|
||||
| features like email verification and password reset.
|
||||
|
|
||||
*/
|
||||
|
||||
'frontend_url' => env('FRONTEND_URL', 'http://localhost:3000'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => 'UTC',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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(',', 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'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Autoloaded Service Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The service providers listed here will be automatically loaded on the
|
||||
| request to your application. Feel free to add your own services to
|
||||
| this array to grant expanded functionality to your applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
/*
|
||||
* Laravel Framework Service Providers...
|
||||
*/
|
||||
Illuminate\Auth\AuthServiceProvider::class,
|
||||
Illuminate\Broadcasting\BroadcastServiceProvider::class,
|
||||
Illuminate\Bus\BusServiceProvider::class,
|
||||
Illuminate\Cache\CacheServiceProvider::class,
|
||||
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
|
||||
Illuminate\Cookie\CookieServiceProvider::class,
|
||||
Illuminate\Database\DatabaseServiceProvider::class,
|
||||
Illuminate\Encryption\EncryptionServiceProvider::class,
|
||||
Illuminate\Filesystem\FilesystemServiceProvider::class,
|
||||
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
|
||||
Illuminate\Hashing\HashServiceProvider::class,
|
||||
Illuminate\Mail\MailServiceProvider::class,
|
||||
Illuminate\Notifications\NotificationServiceProvider::class,
|
||||
Illuminate\Pagination\PaginationServiceProvider::class,
|
||||
Illuminate\Pipeline\PipelineServiceProvider::class,
|
||||
Illuminate\Queue\QueueServiceProvider::class,
|
||||
Illuminate\Redis\RedisServiceProvider::class,
|
||||
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
|
||||
Illuminate\Session\SessionServiceProvider::class,
|
||||
Illuminate\Translation\TranslationServiceProvider::class,
|
||||
Illuminate\Validation\ValidationServiceProvider::class,
|
||||
Illuminate\View\ViewServiceProvider::class,
|
||||
|
||||
/*
|
||||
* Package Service Providers...
|
||||
*/
|
||||
|
||||
/*
|
||||
* Application Service Providers...
|
||||
*/
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\AuthServiceProvider::class,
|
||||
App\Providers\EventServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
],
|
||||
|
||||
];
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'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 amount 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),
|
||||
|
||||
];
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 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", "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',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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(env('APP_NAME', 'laravel'), '_').'_cache_'),
|
||||
|
||||
];
|
|
@ -0,0 +1,177 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'mysql'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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,
|
||||
],
|
||||
|
||||
'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([
|
||||
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([
|
||||
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' => '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(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'),
|
||||
],
|
||||
|
||||
'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'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 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' => env('APP_URL').'/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'),
|
||||
],
|
||||
|
||||
];
|
|
@ -0,0 +1,352 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filename
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The default filename.
|
||||
|
|
||||
*/
|
||||
|
||||
'filename' => '_ide_helper.php',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Models filename
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The default filename for the models helper file.
|
||||
|
|
||||
*/
|
||||
|
||||
'models_filename' => '_ide_helper_models.php',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| PhpStorm meta filename
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| PhpStorm also supports the directory `.phpstorm.meta.php/` with arbitrary
|
||||
| files in it, should you need additional files for your project; e.g.
|
||||
| `.phpstorm.meta.php/laravel_ide_Helper.php'.
|
||||
|
|
||||
*/
|
||||
'meta_filename' => '.phpstorm.meta.php',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fluent helpers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set to true to generate commonly used Fluent methods.
|
||||
|
|
||||
*/
|
||||
|
||||
'include_fluent' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Factory builders
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set to true to generate factory generators for better factory()
|
||||
| method auto-completion.
|
||||
|
|
||||
| Deprecated for Laravel 8 or latest.
|
||||
|
|
||||
*/
|
||||
|
||||
'include_factory_builders' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Write model magic methods
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set to false to disable write magic methods of model.
|
||||
|
|
||||
*/
|
||||
|
||||
'write_model_magic_where' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Write model external Eloquent builder methods
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set to false to disable write external Eloquent builder methods.
|
||||
|
|
||||
*/
|
||||
|
||||
'write_model_external_builder_methods' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Write model relation count properties
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set to false to disable writing of relation count properties to model DocBlocks.
|
||||
|
|
||||
*/
|
||||
|
||||
'write_model_relation_count_properties' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Write Eloquent model mixins
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This will add the necessary DocBlock mixins to the model class
|
||||
| contained in the Laravel framework. This helps the IDE with
|
||||
| auto-completion.
|
||||
|
|
||||
| Please be aware that this setting changes a file within the /vendor directory.
|
||||
|
|
||||
*/
|
||||
|
||||
'write_eloquent_model_mixins' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Helper files to include
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Include helper files. By default not included, but can be toggled with the
|
||||
| -- helpers (-H) option. Extra helper files can be included.
|
||||
|
|
||||
*/
|
||||
|
||||
'include_helpers' => false,
|
||||
|
||||
'helper_files' => [
|
||||
base_path() . '/vendor/laravel/framework/src/Illuminate/Support/helpers.php',
|
||||
base_path() . '/vendor/laravel/framework/src/Illuminate/Foundation/helpers.php',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Model locations to include
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define in which directories the ide-helper:models command should look
|
||||
| for models.
|
||||
|
|
||||
| glob patterns are supported to easier reach models in sub-directories,
|
||||
| e.g. `app/Services/* /Models` (without the space).
|
||||
|
|
||||
*/
|
||||
|
||||
'model_locations' => [
|
||||
'app',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Models to ignore
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define which models should be ignored.
|
||||
|
|
||||
*/
|
||||
|
||||
'ignored_models' => [
|
||||
// App\MyModel::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Models hooks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define which hook classes you want to run for models to add custom information.
|
||||
|
|
||||
| Hooks should implement Barryvdh\LaravelIdeHelper\Contracts\ModelHookInterface.
|
||||
|
|
||||
*/
|
||||
|
||||
'model_hooks' => [
|
||||
// App\Support\IdeHelper\MyModelHook::class
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Extra classes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These implementations are not really extended, but called with magic functions.
|
||||
|
|
||||
*/
|
||||
|
||||
'extra' => [
|
||||
'Eloquent' => ['Illuminate\Database\Eloquent\Builder', 'Illuminate\Database\Query\Builder'],
|
||||
'Session' => ['Illuminate\Session\Store'],
|
||||
],
|
||||
|
||||
'magic' => [],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Interface implementations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These interfaces will be replaced with the implementing class. Some interfaces
|
||||
| are detected by the helpers, others can be listed below.
|
||||
|
|
||||
*/
|
||||
|
||||
'interfaces' => [
|
||||
// App\MyInterface::class => App\MyImplementation::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Support for camel cased models
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| There are some Laravel packages (such as Eloquence) that allow for accessing
|
||||
| Eloquent model properties via camel case, instead of snake case.
|
||||
|
|
||||
| Enabling this option will support these packages by saving all model
|
||||
| properties as camel case, instead of snake case.
|
||||
|
|
||||
| For example, normally you would see this:
|
||||
|
|
||||
| * @property \Illuminate\Support\Carbon $created_at
|
||||
| * @property \Illuminate\Support\Carbon $updated_at
|
||||
|
|
||||
| With this enabled, the properties will be this:
|
||||
|
|
||||
| * @property \Illuminate\Support\Carbon $createdAt
|
||||
| * @property \Illuminate\Support\Carbon $updatedAt
|
||||
|
|
||||
| Note, it is currently an all-or-nothing option.
|
||||
|
|
||||
*/
|
||||
'model_camel_case_properties' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Property casts
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Cast the given "real type" to the given "type".
|
||||
|
|
||||
*/
|
||||
'type_overrides' => [
|
||||
'integer' => 'int',
|
||||
'boolean' => 'bool',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Include DocBlocks from classes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Include DocBlocks from classes to allow additional code inspection for
|
||||
| magic methods and properties.
|
||||
|
|
||||
*/
|
||||
'include_class_docblocks' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Force FQN usage
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Use the fully qualified (class) name in DocBlocks,
|
||||
| even if the class exists in the same namespace
|
||||
| or there is an import (use className) of the class.
|
||||
|
|
||||
*/
|
||||
'force_fqn' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Use generics syntax
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Use generics syntax within DocBlocks,
|
||||
| e.g. `Collection<User>` instead of `Collection|User[]`.
|
||||
|
|
||||
*/
|
||||
'use_generics_annotations' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Additional relation types
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Sometimes it's needed to create custom relation types. The key of the array
|
||||
| is the relationship method name. The value of the array is the fully-qualified
|
||||
| class name of the relationship, e.g. `'relationName' => RelationShipClass::class`.
|
||||
|
|
||||
*/
|
||||
'additional_relation_types' => [],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Additional relation return types
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using custom relation types its possible for the class name to not contain
|
||||
| the proper return type of the relation. The key of the array is the relationship
|
||||
| method name. The value of the array is the return type of the relation ('many'
|
||||
| or 'morphTo').
|
||||
| e.g. `'relationName' => 'many'`.
|
||||
|
|
||||
*/
|
||||
'additional_relation_return_types' => [],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Enforce nullable Eloquent relationships on not null columns
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When set to true (default), this option enforces nullable Eloquent relationships.
|
||||
| However, in cases where the application logic ensures the presence of related
|
||||
| records it may be desirable to set this option to false to avoid unwanted null warnings.
|
||||
|
|
||||
| Default: true
|
||||
| A not null column with no foreign key constraint will have a "nullable" relationship.
|
||||
| * @property int $not_null_column_with_no_foreign_key_constraint
|
||||
| * @property-read BelongsToVariation|null $notNullColumnWithNoForeignKeyConstraint
|
||||
|
|
||||
| Option: false
|
||||
| A not null column with no foreign key constraint will have a "not nullable" relationship.
|
||||
| * @property int $not_null_column_with_no_foreign_key_constraint
|
||||
| * @property-read BelongsToVariation $notNullColumnWithNoForeignKeyConstraint
|
||||
|
|
||||
*/
|
||||
|
||||
'enforce_nullable_relationships' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Run artisan commands after migrations to generate model helpers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The specified commands should run after migrations are finished running.
|
||||
|
|
||||
*/
|
||||
'post_migrate' => [
|
||||
// 'ide-helper:models --nowrite',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Macroable Traits
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define which traits should be considered capable of adding Macro.
|
||||
| You can add any custom trait that behaves like the original Laravel one.
|
||||
|
|
||||
*/
|
||||
'macroable_traits' => [
|
||||
Filament\Support\Concerns\Macroable::class,
|
||||
Spatie\Macroable\Macroable::class,
|
||||
],
|
||||
|
||||
];
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 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(',', 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'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 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(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',
|
||||
],
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
],
|
||||
|
||||
];
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 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", "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,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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',
|
||||
],
|
||||
|
||||
];
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Stateful Domains
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Requests from the following domains / hosts will receive stateful API
|
||||
| authentication cookies. Typically, these should include your local
|
||||
| and production domains which access your API via a frontend SPA.
|
||||
|
|
||||
*/
|
||||
|
||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||
'%s%s',
|
||||
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||
Sanctum::currentApplicationUrlWithPort()
|
||||
))),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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,
|
||||
],
|
||||
|
||||
];
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'token' => env('POSTMARK_TOKEN'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_KEY'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "apc",
|
||||
| "memcached", "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => 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: "apc", "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(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 and all 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),
|
||||
|
||||
];
|
|
@ -0,0 +1 @@
|
|||
*.sqlite*
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->string('password');
|
||||
$table->string('role'); // 'admin', 'penjahit', 'pelanggan'
|
||||
$table->string('phone_number')->nullable();
|
||||
$table->text('address')->nullable();
|
||||
$table->text('shop_description')->nullable(); // Deskripsi toko untuk penjahit
|
||||
$table->string('profile_photo')->nullable(); // Foto profil
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tailor_services', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->string('name');
|
||||
$table->text('description');
|
||||
$table->decimal('price', 10, 2);
|
||||
$table->string('category'); // Bawahan, Atasan, Terusan, Perbaikan
|
||||
$table->integer('estimated_days')->default(1); // Estimasi waktu pengerjaan dalam hari
|
||||
$table->boolean('is_available')->default(true);
|
||||
$table->string('service_photo')->nullable(); // Foto contoh hasil jasa
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tailor_services');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('bookings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('customer_id')->constrained('users')->onDelete('cascade');
|
||||
$table->foreignId('tailor_id')->constrained('users')->onDelete('cascade');
|
||||
$table->date('appointment_date');
|
||||
$table->time('appointment_time');
|
||||
$table->enum('service_type', ['Perbaikan', 'Jahit Baru']);
|
||||
$table->enum('category', ['Atasan', 'Bawahan', 'Terusan']);
|
||||
$table->string('design_photo')->nullable();
|
||||
$table->text('notes')->nullable();
|
||||
$table->enum('status', [
|
||||
'reservasi', // Status awal saat booking dibuat
|
||||
'diproses', // Penjahit sudah menerima dan sedang mengerjakan
|
||||
'selesai', // Jahitan sudah selesai
|
||||
'dibatalkan' // Booking dibatalkan
|
||||
])->default('reservasi');
|
||||
$table->decimal('total_price', 10, 2)->nullable();
|
||||
$table->enum('payment_status', ['unpaid', 'paid'])->default('unpaid');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bookings');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tailor_ratings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('booking_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('customer_id')->constrained('users')->onDelete('cascade');
|
||||
$table->foreignId('tailor_id')->constrained('users')->onDelete('cascade');
|
||||
$table->decimal('rating', 2, 1); // Rating 0.0 - 5.0
|
||||
$table->text('review')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Satu customer hanya bisa memberikan satu rating per booking
|
||||
$table->unique(['booking_id', 'customer_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tailor_ratings');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tailor_specializations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('icon')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Pivot table untuk relasi many-to-many antara penjahit dan spesialisasi
|
||||
Schema::create('tailor_specialization_user', function (Blueprint $table) {
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('tailor_specialization_id')->constrained()->onDelete('cascade');
|
||||
$table->primary(['user_id', 'tailor_specialization_id']);
|
||||
});
|
||||
|
||||
// Insert default specializations
|
||||
DB::table('tailor_specializations')->insert([
|
||||
['name' => 'Celana', 'created_at' => now(), 'updated_at' => now()],
|
||||
['name' => 'Rok', 'created_at' => now(), 'updated_at' => now()],
|
||||
['name' => 'Kemeja', 'created_at' => now(), 'updated_at' => now()],
|
||||
['name' => 'Seragam', 'created_at' => now(), 'updated_at' => now()],
|
||||
['name' => 'Jas Blazer', 'created_at' => now(), 'updated_at' => now()],
|
||||
['name' => 'Kebaya', 'created_at' => now(), 'updated_at' => now()],
|
||||
['name' => 'Gaun', 'created_at' => now(), 'updated_at' => now()],
|
||||
['name' => 'Gamis', 'created_at' => now(), 'updated_at' => now()],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tailor_specialization_user');
|
||||
Schema::dropIfExists('tailor_specializations');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tailor_galleries', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->string('photo');
|
||||
$table->string('title')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->string('category')->nullable(); // Misalnya: Baju Pesta, Kemeja, Celana, dll
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tailor_galleries');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
$table->date('completion_date')->nullable()->after('total_price');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
$table->dropColumn('completion_date');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('password_resets', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('password_resets');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
$table->enum('payment_method', ['tunai', 'transfer_bank', 'ewallet', 'qris'])->nullable()->after('payment_status');
|
||||
$table->string('transaction_code', 20)->nullable()->after('id')->unique();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
$table->dropColumn('payment_method');
|
||||
$table->dropColumn('transaction_code');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Langkah 1: Drop kolom enum payment_method
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
$table->dropColumn('payment_method');
|
||||
});
|
||||
|
||||
// Langkah 2: Tambahkan kolom baru dengan tipe varchar
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
$table->string('payment_method', 50)->nullable()->after('payment_status');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Langkah 1: Drop kolom varchar payment_method
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
$table->dropColumn('payment_method');
|
||||
});
|
||||
|
||||
// Langkah 2: Kembalikan ke kolom enum
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
$table->enum('payment_method', ['tunai', 'transfer_bank', 'ewallet', 'qris'])->nullable()->after('payment_status');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('wallets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->decimal('balance', 12, 2)->default(0);
|
||||
$table->string('status')->default('active');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('wallets');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('bank_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->string('bank_name');
|
||||
$table->string('account_number');
|
||||
$table->string('account_holder_name');
|
||||
$table->string('status')->default('pending'); // pending, active, rejected
|
||||
$table->text('rejection_reason')->nullable();
|
||||
$table->timestamp('verified_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bank_accounts');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('wallet_transactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('wallet_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('booking_id')->nullable()->constrained()->onDelete('set null');
|
||||
$table->string('type'); // credit, debit
|
||||
$table->decimal('amount', 12, 2);
|
||||
$table->string('description');
|
||||
$table->string('status')->default('success'); // success, pending, failed
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('wallet_transactions');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('withdrawals', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('wallet_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('bank_account_id')->constrained()->onDelete('cascade');
|
||||
$table->decimal('amount', 12, 2);
|
||||
$table->string('status')->default('pending'); // pending, processing, completed, rejected
|
||||
$table->text('rejection_reason')->nullable();
|
||||
$table->string('proof_of_payment')->nullable();
|
||||
$table->timestamp('processed_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('withdrawals');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
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('sessions');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('tokenable');
|
||||
$table->string('name');
|
||||
$table->string('token', 64)->unique();
|
||||
$table->text('abilities')->nullable();
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('personal_access_tokens');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
// Tambahkan kolom measurements
|
||||
$table->json('measurements')->nullable()->after('payment_status');
|
||||
|
||||
// Tambahkan kolom lain yang diperlukan berdasarkan controller
|
||||
$table->json('repair_details')->nullable()->after('measurements');
|
||||
$table->string('repair_photo')->nullable()->after('repair_details');
|
||||
$table->text('repair_notes')->nullable()->after('repair_photo');
|
||||
$table->string('completion_photo')->nullable()->after('repair_notes');
|
||||
$table->text('completion_notes')->nullable()->after('completion_photo');
|
||||
|
||||
// Tambahkan kolom untuk tracking waktu
|
||||
$table->timestamp('accepted_at')->nullable()->after('completion_notes');
|
||||
$table->timestamp('rejected_at')->nullable()->after('accepted_at');
|
||||
$table->timestamp('completed_at')->nullable()->after('rejected_at');
|
||||
$table->text('rejection_reason')->nullable()->after('completed_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'measurements',
|
||||
'repair_details',
|
||||
'repair_photo',
|
||||
'repair_notes',
|
||||
'completion_photo',
|
||||
'completion_notes',
|
||||
'accepted_at',
|
||||
'rejected_at',
|
||||
'completed_at',
|
||||
'rejection_reason'
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('customer_specialization', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('tailor_specialization_id')->constrained()->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
|
||||
// Tambahkan unique constraint untuk mencegah duplikasi
|
||||
$table->unique(['user_id', 'tailor_specialization_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('customer_specialization');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->decimal('latitude', 10, 7)->nullable()->after('address');
|
||||
$table->decimal('longitude', 10, 7)->nullable()->after('latitude');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn(['latitude', 'longitude']);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('tailor_specializations', function (Blueprint $table) {
|
||||
$table->string('category')->after('name')->default('Uncategorized');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('tailor_specializations', function (Blueprint $table) {
|
||||
$table->dropColumn('category');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('tailor_specializations', function (Blueprint $table) {
|
||||
$table->string('photo')->nullable()->after('icon');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('tailor_specializations', function (Blueprint $table) {
|
||||
$table->dropColumn('photo');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('tailor_specializations', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('tailor_specializations', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue