add backup file and autobackup code, partial update mutations receive on transation page
This commit is contained in:
356
BACKUP_README.md
Normal file
356
BACKUP_README.md
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
# 🗃️ CKB Database Backup & Restore Guide
|
||||||
|
|
||||||
|
Panduan lengkap untuk backup dan restore database Docker CKB menggunakan mysqldump.
|
||||||
|
|
||||||
|
## 📋 Informasi Database
|
||||||
|
|
||||||
|
- **Container Name**: `ckb-mysql-dev`
|
||||||
|
- **Database Name**: `ckb_db`
|
||||||
|
- **Database Type**: MariaDB 10.6
|
||||||
|
- **Port**: 3306
|
||||||
|
- **Username**: `root` / `laravel`
|
||||||
|
- **Password**: `root` / `password`
|
||||||
|
|
||||||
|
## 🛠️ Available Scripts
|
||||||
|
|
||||||
|
### 1. **backup_db.sh** - Simple Backup
|
||||||
|
|
||||||
|
Script backup sederhana dengan kompresi opsional.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./backup_db.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- ✅ Backup database dengan timestamp
|
||||||
|
- ✅ Kompresi opsional (gzip)
|
||||||
|
- ✅ Validasi container status
|
||||||
|
- ✅ Progress indicator
|
||||||
|
|
||||||
|
### 2. **restore_db.sh** - Database Restore
|
||||||
|
|
||||||
|
Script restore dengan interface interaktif.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./restore_db.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- ✅ List backup files yang tersedia
|
||||||
|
- ✅ Support compressed & uncompressed files
|
||||||
|
- ✅ Konfirmasi sebelum restore
|
||||||
|
- ✅ Automatic decompression
|
||||||
|
|
||||||
|
### 3. **backup_advanced.sh** - Advanced Backup
|
||||||
|
|
||||||
|
Script backup lengkap dengan berbagai opsi.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./backup_advanced.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- ✅ Multiple backup types (Full, Structure, Data)
|
||||||
|
- ✅ Automatic cleanup old backups
|
||||||
|
- ✅ Colored output
|
||||||
|
- ✅ Backup statistics
|
||||||
|
- ✅ Compression with ratio info
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Basic Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simple backup
|
||||||
|
./backup_db.sh
|
||||||
|
|
||||||
|
# Manual backup command
|
||||||
|
docker exec ckb-mysql-dev mysqldump -u root -proot ckb_db > backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive advanced backup
|
||||||
|
./backup_advanced.sh
|
||||||
|
|
||||||
|
# Full backup with all options
|
||||||
|
docker exec ckb-mysql-dev mysqldump -u root -proot \
|
||||||
|
--single-transaction \
|
||||||
|
--routines \
|
||||||
|
--triggers \
|
||||||
|
--add-drop-table \
|
||||||
|
ckb_db > full_backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive restore
|
||||||
|
./restore_db.sh
|
||||||
|
|
||||||
|
# Manual restore
|
||||||
|
docker exec -i ckb-mysql-dev mysql -u root -proot ckb_db < backup.sql
|
||||||
|
|
||||||
|
# Restore compressed backup
|
||||||
|
gunzip -c backup.sql.gz | docker exec -i ckb-mysql-dev mysql -u root -proot ckb_db
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📂 Backup Types
|
||||||
|
|
||||||
|
### 1. **Full Backup** (Default)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec ckb-mysql-dev mysqldump -u root -proot \
|
||||||
|
--single-transaction \
|
||||||
|
--routines \
|
||||||
|
--triggers \
|
||||||
|
--add-drop-table \
|
||||||
|
ckb_db > full_backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Structure Only**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec ckb-mysql-dev mysqldump -u root -proot \
|
||||||
|
--no-data \
|
||||||
|
--routines \
|
||||||
|
--triggers \
|
||||||
|
ckb_db > structure_backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Data Only**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec ckb-mysql-dev mysqldump -u root -proot \
|
||||||
|
--no-create-info \
|
||||||
|
--single-transaction \
|
||||||
|
ckb_db > data_backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Compressed Backup**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec ckb-mysql-dev mysqldump -u root -proot ckb_db | gzip > backup.sql.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ Manual Commands
|
||||||
|
|
||||||
|
### Basic Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check container status
|
||||||
|
docker ps | grep ckb-mysql-dev
|
||||||
|
|
||||||
|
# Backup with timestamp
|
||||||
|
docker exec ckb-mysql-dev mysqldump -u root -proot ckb_db > ckb_backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
|
||||||
|
# Backup all databases
|
||||||
|
docker exec ckb-mysql-dev mysqldump -u root -proot --all-databases > all_databases.sql
|
||||||
|
|
||||||
|
# Backup specific tables
|
||||||
|
docker exec ckb-mysql-dev mysqldump -u root -proot ckb_db table1 table2 > specific_tables.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup with extended options
|
||||||
|
docker exec ckb-mysql-dev mysqldump -u root -proot \
|
||||||
|
--single-transaction \
|
||||||
|
--routines \
|
||||||
|
--triggers \
|
||||||
|
--events \
|
||||||
|
--add-drop-table \
|
||||||
|
--add-drop-trigger \
|
||||||
|
--hex-blob \
|
||||||
|
--complete-insert \
|
||||||
|
ckb_db > advanced_backup.sql
|
||||||
|
|
||||||
|
# Backup without locking tables (for MyISAM)
|
||||||
|
docker exec ckb-mysql-dev mysqldump -u root -proot \
|
||||||
|
--lock-tables=false \
|
||||||
|
ckb_db > no_lock_backup.sql
|
||||||
|
|
||||||
|
# Backup with conditions
|
||||||
|
docker exec ckb-mysql-dev mysqldump -u root -proot \
|
||||||
|
--where="created_at >= '2023-01-01'" \
|
||||||
|
ckb_db table_name > conditional_backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Automated Backups
|
||||||
|
|
||||||
|
### Cron Job Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Edit crontab
|
||||||
|
crontab -e
|
||||||
|
|
||||||
|
# Daily backup at 2 AM
|
||||||
|
0 2 * * * /path/to/backup_db.sh
|
||||||
|
|
||||||
|
# Weekly full backup on Sunday at 3 AM
|
||||||
|
0 3 * * 0 /path/to/backup_advanced.sh
|
||||||
|
|
||||||
|
# Monthly cleanup (keep last 30 days)
|
||||||
|
0 4 1 * * find /path/to/backups -name "*.sql*" -mtime +30 -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
### Systemd Timer (Alternative to Cron)
|
||||||
|
|
||||||
|
Create `/etc/systemd/system/ckb-backup.service`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=CKB Database Backup
|
||||||
|
After=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/path/to/backup_db.sh
|
||||||
|
User=root
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `/etc/systemd/system/ckb-backup.timer`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Run CKB backup daily
|
||||||
|
Requires=ckb-backup.service
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=daily
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable timer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable ckb-backup.timer
|
||||||
|
sudo systemctl start ckb-backup.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Backup Management
|
||||||
|
|
||||||
|
### Check Backup Size
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show backup directory size
|
||||||
|
du -sh backups/
|
||||||
|
|
||||||
|
# List all backups with sizes
|
||||||
|
ls -lah backups/
|
||||||
|
|
||||||
|
# Show compression ratio
|
||||||
|
for file in backups/*.sql.gz; do
|
||||||
|
original_size=$(gunzip -l "$file" | tail -1 | awk '{print $2}')
|
||||||
|
compressed_size=$(stat -c%s "$file")
|
||||||
|
ratio=$(echo "scale=1; (1 - $compressed_size/$original_size) * 100" | bc)
|
||||||
|
echo "$(basename "$file"): ${ratio}% compression"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cleanup Old Backups
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Delete backups older than 30 days
|
||||||
|
find backups/ -name "*.sql*" -mtime +30 -delete
|
||||||
|
|
||||||
|
# Keep only last 10 backups
|
||||||
|
ls -1t backups/ckb_backup_*.sql* | tail -n +11 | xargs rm -f
|
||||||
|
|
||||||
|
# Clean by size (keep if total < 1GB)
|
||||||
|
total_size=$(du -s backups/ | cut -f1)
|
||||||
|
if [ $total_size -gt 1048576 ]; then
|
||||||
|
# Delete oldest files until under 1GB
|
||||||
|
echo "Cleaning up old backups..."
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚨 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Container not running**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start container
|
||||||
|
docker-compose up -d db
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker logs ckb-mysql-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Permission denied**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fix script permissions
|
||||||
|
chmod +x *.sh
|
||||||
|
|
||||||
|
# Fix backup directory permissions
|
||||||
|
sudo chown -R $USER:$USER backups/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Out of disk space**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check disk usage
|
||||||
|
df -h
|
||||||
|
|
||||||
|
# Clean old backups
|
||||||
|
find backups/ -name "*.sql*" -mtime +7 -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **MySQL connection error**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test connection
|
||||||
|
docker exec ckb-mysql-dev mysql -u root -proot -e "SELECT 1;"
|
||||||
|
|
||||||
|
# Check MySQL status
|
||||||
|
docker exec ckb-mysql-dev mysqladmin -u root -proot status
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Security Notes
|
||||||
|
|
||||||
|
1. **Never store passwords in plain text** - Consider using MySQL config files
|
||||||
|
2. **Encrypt sensitive backups** - Use GPG for production environments
|
||||||
|
3. **Secure backup storage** - Store backups in secure, offsite locations
|
||||||
|
4. **Regular restore testing** - Test backups regularly to ensure they work
|
||||||
|
|
||||||
|
## 📈 Best Practices
|
||||||
|
|
||||||
|
1. **Regular Backups**: Daily for production, weekly for development
|
||||||
|
2. **Multiple Backup Types**: Keep both full and incremental backups
|
||||||
|
3. **Offsite Storage**: Store backups in different physical/cloud locations
|
||||||
|
4. **Compression**: Use gzip to save storage space
|
||||||
|
5. **Retention Policy**: Define how long to keep backups
|
||||||
|
6. **Monitoring**: Monitor backup success/failure
|
||||||
|
7. **Documentation**: Document backup procedures and recovery steps
|
||||||
|
8. **Testing**: Regularly test restore procedures
|
||||||
|
|
||||||
|
## 📝 File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
backups/
|
||||||
|
├── ckb_backup_20231225_143022.sql.gz # Full backup (compressed)
|
||||||
|
├── ckb_structure_20231225_143022.sql # Structure only
|
||||||
|
├── ckb_data_20231225_143022.sql.gz # Data only (compressed)
|
||||||
|
└── README.md # This documentation
|
||||||
|
|
||||||
|
Scripts:
|
||||||
|
├── backup_db.sh # Simple backup script
|
||||||
|
├── backup_advanced.sh # Advanced backup with options
|
||||||
|
├── restore_db.sh # Interactive restore script
|
||||||
|
└── BACKUP_README.md # This documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Made with ❤️ for CKB Project**
|
||||||
@@ -288,4 +288,90 @@ class MutationsController extends Controller
|
|||||||
'current_stock' => $stock
|
'current_stock' => $stock
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API untuk mendapatkan mutasi yang perlu diterima oleh dealer
|
||||||
|
public function getPendingMutations(Request $request)
|
||||||
|
{
|
||||||
|
$dealerId = $request->dealer_id;
|
||||||
|
|
||||||
|
$data = Mutation::with(['fromDealer', 'toDealer', 'requestedBy.role'])
|
||||||
|
->where('to_dealer_id', $dealerId)
|
||||||
|
->where('status', 'sent')
|
||||||
|
->select('mutations.*');
|
||||||
|
|
||||||
|
return DataTables::of($data)
|
||||||
|
->addIndexColumn()
|
||||||
|
->addColumn('mutation_number', function($row) {
|
||||||
|
return $row->mutation_number;
|
||||||
|
})
|
||||||
|
->addColumn('from_dealer', function($row) {
|
||||||
|
return $row->fromDealer->name ?? '-';
|
||||||
|
})
|
||||||
|
->addColumn('status', function($row) {
|
||||||
|
$statusColor = $row->status_color;
|
||||||
|
$statusLabel = $row->status_label;
|
||||||
|
|
||||||
|
$textColorClass = match($statusColor) {
|
||||||
|
'success' => 'text-success',
|
||||||
|
'warning' => 'text-warning',
|
||||||
|
'danger' => 'text-danger',
|
||||||
|
'info' => 'text-info',
|
||||||
|
'primary' => 'text-primary',
|
||||||
|
'brand' => 'text-primary',
|
||||||
|
'secondary' => 'text-muted',
|
||||||
|
default => 'text-dark'
|
||||||
|
};
|
||||||
|
|
||||||
|
return "<span class=\"font-weight-bold {$textColorClass}\">{$statusLabel}</span>";
|
||||||
|
})
|
||||||
|
->addColumn('total_items', function($row) {
|
||||||
|
return number_format($row->total_items, 0);
|
||||||
|
})
|
||||||
|
->addColumn('created_at', function($row) {
|
||||||
|
return $row->created_at->format('d/m/Y H:i');
|
||||||
|
})
|
||||||
|
->addColumn('action', function($row) {
|
||||||
|
return '<button type="button" class="btn btn-info btn-sm btn-detail" onclick="showMutationDetail('.$row->id.')">
|
||||||
|
Detail
|
||||||
|
</button>';
|
||||||
|
})
|
||||||
|
->rawColumns(['status', 'action'])
|
||||||
|
->make(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// API untuk mendapatkan detail mutasi
|
||||||
|
public function getDetail(Mutation $mutation)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$mutation->load([
|
||||||
|
'fromDealer',
|
||||||
|
'toDealer',
|
||||||
|
'requestedBy.role',
|
||||||
|
'approvedBy.role',
|
||||||
|
'receivedBy.role',
|
||||||
|
'mutationDetails.product'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Format created_at
|
||||||
|
$mutation->created_at_formatted = $mutation->created_at->format('d/m/Y H:i');
|
||||||
|
|
||||||
|
// Add status color and label
|
||||||
|
$mutation->status_color = $mutation->status_color;
|
||||||
|
$mutation->status_label = $mutation->status_label;
|
||||||
|
|
||||||
|
// Check if can be received
|
||||||
|
$mutation->can_be_received = $mutation->canBeReceived();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $mutation
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Gagal memuat detail mutasi: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
253
backup_advanced.sh
Executable file
253
backup_advanced.sh
Executable file
@@ -0,0 +1,253 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Advanced Database Backup Script for CKB Docker
|
||||||
|
# ===============================================
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
CONTAINER_NAME="ckb-mysql-dev"
|
||||||
|
DB_NAME="ckb_db"
|
||||||
|
DB_USER="root"
|
||||||
|
DB_PASSWORD="root"
|
||||||
|
BACKUP_DIR="./backups"
|
||||||
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
RETENTION_DAYS=30 # Keep backups for 30 days
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
print_header() {
|
||||||
|
echo -e "${BLUE}================================================${NC}"
|
||||||
|
echo -e "${BLUE} CKB Database Advanced Backup Tool${NC}"
|
||||||
|
echo -e "${BLUE}================================================${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info() {
|
||||||
|
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_container() {
|
||||||
|
if ! docker ps | grep -q $CONTAINER_NAME; then
|
||||||
|
print_error "Container $CONTAINER_NAME is not running!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
print_success "Container $CONTAINER_NAME is running"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_backup_dir() {
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
print_info "Backup directory: $BACKUP_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_old_backups() {
|
||||||
|
print_info "Cleaning up backups older than $RETENTION_DAYS days..."
|
||||||
|
|
||||||
|
# Find and delete old backup files
|
||||||
|
old_files=$(find $BACKUP_DIR -name "ckb_backup_*.sql*" -mtime +$RETENTION_DAYS 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -z "$old_files" ]]; then
|
||||||
|
print_info "No old backup files found"
|
||||||
|
else
|
||||||
|
echo "$old_files" | while read file; do
|
||||||
|
rm -f "$file"
|
||||||
|
print_warning "Deleted old backup: $(basename "$file")"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_full() {
|
||||||
|
local backup_file="$BACKUP_DIR/ckb_backup_${DATE}.sql"
|
||||||
|
print_info "Creating full backup..."
|
||||||
|
|
||||||
|
if docker exec $CONTAINER_NAME mysqldump -u $DB_USER -p$DB_PASSWORD \
|
||||||
|
--single-transaction \
|
||||||
|
--routines \
|
||||||
|
--triggers \
|
||||||
|
--add-drop-table \
|
||||||
|
$DB_NAME > "$backup_file"; then
|
||||||
|
|
||||||
|
print_success "Full backup created: $backup_file"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Full backup failed!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_structure_only() {
|
||||||
|
local backup_file="$BACKUP_DIR/ckb_structure_${DATE}.sql"
|
||||||
|
print_info "Creating structure-only backup..."
|
||||||
|
|
||||||
|
if docker exec $CONTAINER_NAME mysqldump -u $DB_USER -p$DB_PASSWORD \
|
||||||
|
--no-data \
|
||||||
|
--routines \
|
||||||
|
--triggers \
|
||||||
|
$DB_NAME > "$backup_file"; then
|
||||||
|
|
||||||
|
print_success "Structure backup created: $backup_file"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Structure backup failed!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_data_only() {
|
||||||
|
local backup_file="$BACKUP_DIR/ckb_data_${DATE}.sql"
|
||||||
|
print_info "Creating data-only backup..."
|
||||||
|
|
||||||
|
if docker exec $CONTAINER_NAME mysqldump -u $DB_USER -p$DB_PASSWORD \
|
||||||
|
--no-create-info \
|
||||||
|
--single-transaction \
|
||||||
|
$DB_NAME > "$backup_file"; then
|
||||||
|
|
||||||
|
print_success "Data backup created: $backup_file"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Data backup failed!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
compress_backup() {
|
||||||
|
local file="$1"
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
print_info "Compressing backup..."
|
||||||
|
if gzip "$file"; then
|
||||||
|
print_success "Backup compressed: $file.gz"
|
||||||
|
|
||||||
|
# Show compression ratio
|
||||||
|
original_size=$(stat -c%s "$file" 2>/dev/null || echo "0")
|
||||||
|
compressed_size=$(stat -c%s "$file.gz" 2>/dev/null || echo "0")
|
||||||
|
if [[ $original_size -gt 0 ]]; then
|
||||||
|
ratio=$(echo "scale=1; (1 - $compressed_size/$original_size) * 100" | bc -l 2>/dev/null || echo "0")
|
||||||
|
print_info "Compression ratio: ${ratio}%"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Compression failed!"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_backup_stats() {
|
||||||
|
print_info "Backup Statistics:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
# Count backup files
|
||||||
|
total_backups=$(ls -1 $BACKUP_DIR/ckb_backup_*.sql* 2>/dev/null | wc -l)
|
||||||
|
total_size=$(du -sh $BACKUP_DIR 2>/dev/null | cut -f1)
|
||||||
|
|
||||||
|
echo "Total backups: $total_backups"
|
||||||
|
echo "Total size: $total_size"
|
||||||
|
echo "Newest backup: $(ls -1t $BACKUP_DIR/ckb_backup_*.sql* 2>/dev/null | head -1 | xargs basename 2>/dev/null || echo "None")"
|
||||||
|
echo "Oldest backup: $(ls -1tr $BACKUP_DIR/ckb_backup_*.sql* 2>/dev/null | head -1 | xargs basename 2>/dev/null || echo "None")"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main Script
|
||||||
|
print_header
|
||||||
|
|
||||||
|
echo "Backup Options:"
|
||||||
|
echo "1. Full backup (structure + data)"
|
||||||
|
echo "2. Structure only backup"
|
||||||
|
echo "3. Data only backup"
|
||||||
|
echo "4. All backup types"
|
||||||
|
echo "5. Show backup statistics"
|
||||||
|
echo "6. Cleanup old backups"
|
||||||
|
echo "7. Exit"
|
||||||
|
echo
|
||||||
|
|
||||||
|
read -p "Choose option (1-7): " -n 1 -r option
|
||||||
|
echo
|
||||||
|
|
||||||
|
case $option in
|
||||||
|
1)
|
||||||
|
check_container
|
||||||
|
create_backup_dir
|
||||||
|
if backup_full; then
|
||||||
|
backup_file="$BACKUP_DIR/ckb_backup_${DATE}.sql"
|
||||||
|
|
||||||
|
read -p "Compress backup? (y/n): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
compress_backup "$backup_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup_old_backups
|
||||||
|
show_backup_stats
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
check_container
|
||||||
|
create_backup_dir
|
||||||
|
if backup_structure_only; then
|
||||||
|
backup_file="$BACKUP_DIR/ckb_structure_${DATE}.sql"
|
||||||
|
|
||||||
|
read -p "Compress backup? (y/n): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
compress_backup "$backup_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
check_container
|
||||||
|
create_backup_dir
|
||||||
|
if backup_data_only; then
|
||||||
|
backup_file="$BACKUP_DIR/ckb_data_${DATE}.sql"
|
||||||
|
|
||||||
|
read -p "Compress backup? (y/n): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
compress_backup "$backup_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
check_container
|
||||||
|
create_backup_dir
|
||||||
|
|
||||||
|
print_info "Creating all backup types..."
|
||||||
|
|
||||||
|
backup_full && compress_backup "$BACKUP_DIR/ckb_backup_${DATE}.sql"
|
||||||
|
backup_structure_only && compress_backup "$BACKUP_DIR/ckb_structure_${DATE}.sql"
|
||||||
|
backup_data_only && compress_backup "$BACKUP_DIR/ckb_data_${DATE}.sql"
|
||||||
|
|
||||||
|
cleanup_old_backups
|
||||||
|
show_backup_stats
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
create_backup_dir
|
||||||
|
show_backup_stats
|
||||||
|
;;
|
||||||
|
6)
|
||||||
|
create_backup_dir
|
||||||
|
cleanup_old_backups
|
||||||
|
show_backup_stats
|
||||||
|
;;
|
||||||
|
7)
|
||||||
|
print_info "Goodbye!"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Invalid option!"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
print_success "Backup process completed!"
|
||||||
48
backup_db.sh
Executable file
48
backup_db.sh
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
CONTAINER_NAME="ckb-mysql-dev"
|
||||||
|
DB_NAME="ckb_db"
|
||||||
|
DB_USER="root"
|
||||||
|
DB_PASSWORD="root"
|
||||||
|
BACKUP_DIR="./backups"
|
||||||
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
BACKUP_FILE="ckb_backup_${DATE}.sql"
|
||||||
|
|
||||||
|
# Create backup directory if it doesn't exist
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
|
||||||
|
# Check if container is running
|
||||||
|
if ! docker ps | grep -q $CONTAINER_NAME; then
|
||||||
|
echo "Error: Container $CONTAINER_NAME is not running!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting database backup..."
|
||||||
|
echo "Container: $CONTAINER_NAME"
|
||||||
|
echo "Database: $DB_NAME"
|
||||||
|
echo "Backup file: $BACKUP_DIR/$BACKUP_FILE"
|
||||||
|
|
||||||
|
# Create backup
|
||||||
|
if docker exec $CONTAINER_NAME mysqldump -u $DB_USER -p$DB_PASSWORD $DB_NAME > "$BACKUP_DIR/$BACKUP_FILE"; then
|
||||||
|
echo "✅ Backup completed successfully!"
|
||||||
|
echo "📁 File saved: $BACKUP_DIR/$BACKUP_FILE"
|
||||||
|
|
||||||
|
# Show file size
|
||||||
|
FILE_SIZE=$(du -h "$BACKUP_DIR/$BACKUP_FILE" | cut -f1)
|
||||||
|
echo "📊 File size: $FILE_SIZE"
|
||||||
|
|
||||||
|
# Optional: Compress the backup
|
||||||
|
read -p "Do you want to compress the backup? (y/n): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
gzip "$BACKUP_DIR/$BACKUP_FILE"
|
||||||
|
echo "🗜️ Backup compressed: $BACKUP_DIR/$BACKUP_FILE.gz"
|
||||||
|
fi
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "❌ Backup failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🎉 Backup process completed!"
|
||||||
BIN
backups/ckb_backup_20250612_175715.sql.gz
Normal file
BIN
backups/ckb_backup_20250612_175715.sql.gz
Normal file
Binary file not shown.
@@ -108,6 +108,62 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
.available-stock-mutasi {
|
.available-stock-mutasi {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Styles for receive mutations table */
|
||||||
|
#receiveMutationsTable {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#receiveMutationsTable th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#receiveMutationsTable td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-detail {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mutation-detail-table {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mutation-detail-table th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-approved-input {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-approved-input.is-invalid {
|
||||||
|
border-color: #dc3545;
|
||||||
|
background-color: #fff5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mutation-detail-table textarea {
|
||||||
|
font-size: 12px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mutation-detail-table input {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@@ -487,6 +543,9 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" data-toggle="tab" href="#mutasi">Mutasi</a>
|
<a class="nav-link" data-toggle="tab" href="#mutasi">Mutasi</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" data-toggle="tab" href="#penerimaan">Penerimaan Mutasi</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content mt-3">
|
<div class="tab-content mt-3">
|
||||||
@@ -655,6 +714,30 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Penerimaan Mutasi -->
|
||||||
|
<div class="tab-pane" id="penerimaan" role="tabpanel">
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6 class="mb-3">Daftar Mutasi yang Perlu Diterima</h6>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-hover" id="receiveMutationsTable">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th width="20%">No. Mutasi</th>
|
||||||
|
<th width="25%">Dealer Asal</th>
|
||||||
|
<th width="15%">Status</th>
|
||||||
|
<th width="10%">Total Item</th>
|
||||||
|
<th width="15%">Tanggal</th>
|
||||||
|
<th width="15%">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- Data will be loaded via DataTables AJAX -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -668,6 +751,37 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
<input type="hidden" name="dealer_id" value="{{ $mechanic->dealer_id }}">
|
<input type="hidden" name="dealer_id" value="{{ $mechanic->dealer_id }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Detail Mutasi -->
|
||||||
|
<div class="modal fade" id="mutationDetailModal" tabindex="-1" role="dialog" aria-labelledby="mutationDetailModalLabel">
|
||||||
|
<div class="modal-dialog modal-xl" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="mutationDetailModalLabel">Detail & Penerimaan Mutasi</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form id="receiveMutationForm" action="" method="POST">
|
||||||
|
@csrf
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="mutationDetailContent">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="fa fa-spinner fa-spin fa-2x"></i>
|
||||||
|
<p class="mt-2">Memuat data...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Tutup</button>
|
||||||
|
<button type="submit" class="btn btn-success" id="receiveButton" style="display: none;">
|
||||||
|
<i class="fa fa-check"></i> Terima Mutasi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('javascripts')
|
@section('javascripts')
|
||||||
@@ -1479,6 +1593,277 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Initialize DataTable for receive mutations
|
||||||
|
var receiveMutationsTable;
|
||||||
|
|
||||||
|
function initReceiveMutationsTable() {
|
||||||
|
if (receiveMutationsTable) {
|
||||||
|
receiveMutationsTable.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
receiveMutationsTable = $('#receiveMutationsTable').DataTable({
|
||||||
|
processing: true,
|
||||||
|
serverSide: true,
|
||||||
|
ajax: {
|
||||||
|
url: '{{ route("mutations.get-pending-mutations") }}',
|
||||||
|
data: {
|
||||||
|
dealer_id: {{ $mechanic->dealer_id }}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{data: 'mutation_number', name: 'mutation_number'},
|
||||||
|
{data: 'from_dealer', name: 'from_dealer'},
|
||||||
|
{data: 'status', name: 'status', orderable: false},
|
||||||
|
{data: 'total_items', name: 'total_items'},
|
||||||
|
{data: 'created_at', name: 'created_at'},
|
||||||
|
{data: 'action', name: 'action', orderable: false, searchable: false}
|
||||||
|
],
|
||||||
|
pageLength: 10,
|
||||||
|
responsive: true,
|
||||||
|
scrollX: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show mutation detail modal
|
||||||
|
function showMutationDetail(mutationId) {
|
||||||
|
$('#mutationDetailModal').modal('show');
|
||||||
|
$('#mutationDetailContent').html(`
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="fa fa-spinner fa-spin fa-2x"></i>
|
||||||
|
<p class="mt-2">Memuat data...</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
$('#receiveButton').hide();
|
||||||
|
|
||||||
|
// Load mutation detail via AJAX
|
||||||
|
$.ajax({
|
||||||
|
url: '{{ route("mutations.get-detail", ":id") }}'.replace(':id', mutationId),
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
renderMutationDetail(response.data);
|
||||||
|
} else {
|
||||||
|
$('#mutationDetailContent').html(`
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<i class="fa fa-exclamation-triangle"></i>
|
||||||
|
${response.message || 'Gagal memuat detail mutasi'}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
$('#mutationDetailContent').html(`
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<i class="fa fa-exclamation-triangle"></i>
|
||||||
|
Terjadi kesalahan saat memuat data
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render mutation detail in modal
|
||||||
|
function renderMutationDetail(mutation) {
|
||||||
|
var statusColor = mutation.status_color;
|
||||||
|
var statusLabel = mutation.status_label;
|
||||||
|
|
||||||
|
// Set form action URL
|
||||||
|
$('#receiveMutationForm').attr('action', '{{ route("mutations.receive", ":id") }}'.replace(':id', mutation.id));
|
||||||
|
|
||||||
|
// Build detail HTML
|
||||||
|
var detailHtml = `
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>No. Mutasi:</strong><br>
|
||||||
|
${mutation.mutation_number}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Status:</strong><br>
|
||||||
|
<span class="font-weight-bold text-${statusColor}">${statusLabel}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Dealer Asal:</strong><br>
|
||||||
|
${mutation.from_dealer.name}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Tanggal:</strong><br>
|
||||||
|
${mutation.created_at_formatted}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<strong>Diminta oleh:</strong><br>
|
||||||
|
${mutation.requested_by ? mutation.requested_by.name : '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label for="mutationNotes"><strong>Catatan Penerimaan:</strong></label>
|
||||||
|
<textarea name="notes" id="mutationNotes" class="form-control" rows="3" placeholder="Masukkan catatan jika diperlukan..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="mb-3">Detail Produk & Penerimaan:</h6>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered mutation-detail-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="30%">Produk</th>
|
||||||
|
<th width="15%" class="text-center">Qty Diminta</th>
|
||||||
|
<th width="20%" class="text-center">Qty Disetujui</th>
|
||||||
|
<th width="35%">Catatan Produk</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add product details with form inputs
|
||||||
|
if (mutation.mutation_details && mutation.mutation_details.length > 0) {
|
||||||
|
mutation.mutation_details.forEach(function(detail) {
|
||||||
|
detailHtml += `
|
||||||
|
<tr>
|
||||||
|
<td>${detail.product.name}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="font-weight-bold text-info">${parseFloat(detail.quantity_requested).toFixed(2)}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number"
|
||||||
|
name="products[${detail.id}][quantity_approved]"
|
||||||
|
class="form-control quantity-approved-input"
|
||||||
|
min="0"
|
||||||
|
max="${detail.quantity_requested}"
|
||||||
|
step="0.01"
|
||||||
|
value="${detail.quantity_requested}"
|
||||||
|
data-max="${detail.quantity_requested}"
|
||||||
|
placeholder="0.00">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<textarea name="products[${detail.id}][notes]"
|
||||||
|
class="form-control"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Catatan untuk produk ini..."></textarea>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
detailHtml += `
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center">Tidak ada detail produk</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
detailHtml += `
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fa fa-info-circle"></i>
|
||||||
|
<strong>Petunjuk:</strong>
|
||||||
|
Masukkan quantity yang disetujui untuk setiap produk. Quantity tidak boleh melebihi quantity yang diminta.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$('#mutationDetailContent').html(detailHtml);
|
||||||
|
|
||||||
|
// Show receive button if mutation can be received
|
||||||
|
if (mutation.can_be_received) {
|
||||||
|
$('#receiveButton').show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add validation for quantity inputs
|
||||||
|
$('.quantity-approved-input').on('input', function() {
|
||||||
|
var value = parseFloat($(this).val()) || 0;
|
||||||
|
var max = parseFloat($(this).data('max')) || 0;
|
||||||
|
|
||||||
|
if (value > max) {
|
||||||
|
$(this).addClass('is-invalid');
|
||||||
|
if (!$(this).siblings('.invalid-feedback').length) {
|
||||||
|
$(this).after('<div class="invalid-feedback">Quantity tidak boleh melebihi quantity yang diminta</div>');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$(this).removeClass('is-invalid');
|
||||||
|
$(this).siblings('.invalid-feedback').remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle receive form submission
|
||||||
|
$('#receiveMutationForm').on('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Validate form
|
||||||
|
var hasInvalidInput = $('.quantity-approved-input.is-invalid').length > 0;
|
||||||
|
if (hasInvalidInput) {
|
||||||
|
Swal.fire({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Validasi Gagal',
|
||||||
|
text: 'Perbaiki quantity yang tidak valid sebelum melanjutkan'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if at least one product has quantity approved > 0
|
||||||
|
var hasApprovedQuantity = false;
|
||||||
|
$('.quantity-approved-input').each(function() {
|
||||||
|
if (parseFloat($(this).val()) > 0) {
|
||||||
|
hasApprovedQuantity = true;
|
||||||
|
return false; // break loop
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasApprovedQuantity) {
|
||||||
|
Swal.fire({
|
||||||
|
type: 'warning',
|
||||||
|
title: 'Peringatan',
|
||||||
|
text: 'Minimal satu produk harus memiliki quantity yang disetujui'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Swal !== 'undefined') {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Terima Mutasi?',
|
||||||
|
text: "Mutasi akan diterima dengan quantity dan catatan yang telah Anda masukkan",
|
||||||
|
type: 'question',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#28a745',
|
||||||
|
cancelButtonColor: '#6c757d',
|
||||||
|
confirmButtonText: 'Ya, Terima',
|
||||||
|
cancelButtonText: 'Batal'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.value) {
|
||||||
|
// Set loading state
|
||||||
|
$('#receiveButton').prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Memproses...');
|
||||||
|
|
||||||
|
// Submit form
|
||||||
|
this.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (confirm('Terima mutasi dengan data yang telah dimasukkan?')) {
|
||||||
|
$('#receiveButton').prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Memproses...');
|
||||||
|
this.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize table when tab is shown
|
||||||
|
$('a[href="#penerimaan"]').on('shown.bs.tab', function (e) {
|
||||||
|
setTimeout(function() {
|
||||||
|
initReceiveMutationsTable();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
function createTransaction(form) {
|
function createTransaction(form) {
|
||||||
let work_ids;
|
let work_ids;
|
||||||
if(form == 'work') {
|
if(form == 'work') {
|
||||||
|
|||||||
127
restore_db.sh
Executable file
127
restore_db.sh
Executable file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
CONTAINER_NAME="ckb-mysql-dev"
|
||||||
|
DB_NAME="ckb_db"
|
||||||
|
DB_USER="root"
|
||||||
|
DB_PASSWORD="root"
|
||||||
|
BACKUP_DIR="./backups"
|
||||||
|
|
||||||
|
# Function to show available backups
|
||||||
|
show_backups() {
|
||||||
|
echo "📁 Available backup files:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
ls -la "$BACKUP_DIR"/*.sql* 2>/dev/null | awk '{print NR ". " $9 " (" $5 " bytes, " $6 " " $7 " " $8 ")"}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to restore database
|
||||||
|
restore_database() {
|
||||||
|
local backup_file="$1"
|
||||||
|
|
||||||
|
echo "🔄 Starting database restore..."
|
||||||
|
echo "Container: $CONTAINER_NAME"
|
||||||
|
echo "Database: $DB_NAME"
|
||||||
|
echo "Backup file: $backup_file"
|
||||||
|
|
||||||
|
# Check if file exists
|
||||||
|
if [[ ! -f "$backup_file" ]]; then
|
||||||
|
echo "❌ Error: Backup file not found: $backup_file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if container is running
|
||||||
|
if ! docker ps | grep -q $CONTAINER_NAME; then
|
||||||
|
echo "❌ Error: Container $CONTAINER_NAME is not running!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ask for confirmation
|
||||||
|
read -p "⚠️ This will overwrite the current database. Continue? (y/n): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "🚫 Restore cancelled."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if file is compressed
|
||||||
|
if [[ "$backup_file" == *.gz ]]; then
|
||||||
|
echo "📦 Decompressing backup file..."
|
||||||
|
if gunzip -c "$backup_file" | docker exec -i $CONTAINER_NAME mysql -u $DB_USER -p$DB_PASSWORD $DB_NAME; then
|
||||||
|
echo "✅ Database restored successfully from compressed backup!"
|
||||||
|
else
|
||||||
|
echo "❌ Restore failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "📥 Restoring from uncompressed backup..."
|
||||||
|
if docker exec -i $CONTAINER_NAME mysql -u $DB_USER -p$DB_PASSWORD $DB_NAME < "$backup_file"; then
|
||||||
|
echo "✅ Database restored successfully!"
|
||||||
|
else
|
||||||
|
echo "❌ Restore failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main script
|
||||||
|
echo "🗃️ CKB Database Restore Tool"
|
||||||
|
echo "============================"
|
||||||
|
|
||||||
|
# Check if backup directory exists
|
||||||
|
if [[ ! -d "$BACKUP_DIR" ]]; then
|
||||||
|
echo "❌ Error: Backup directory not found: $BACKUP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show available backups
|
||||||
|
show_backups
|
||||||
|
|
||||||
|
# Check if any backup files exist
|
||||||
|
if ! ls "$BACKUP_DIR"/*.sql* 1> /dev/null 2>&1; then
|
||||||
|
echo "❌ No backup files found in $BACKUP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Options:"
|
||||||
|
echo "1. Select backup file by number"
|
||||||
|
echo "2. Enter custom backup file path"
|
||||||
|
echo "3. Exit"
|
||||||
|
echo
|
||||||
|
|
||||||
|
read -p "Choose option (1-3): " -n 1 -r option
|
||||||
|
echo
|
||||||
|
|
||||||
|
case $option in
|
||||||
|
1)
|
||||||
|
echo "📋 Available backups:"
|
||||||
|
backup_files=($(ls "$BACKUP_DIR"/*.sql* 2>/dev/null))
|
||||||
|
for i in "${!backup_files[@]}"; do
|
||||||
|
echo "$((i+1)). $(basename "${backup_files[$i]}")"
|
||||||
|
done
|
||||||
|
|
||||||
|
read -p "Enter backup number: " backup_number
|
||||||
|
|
||||||
|
if [[ "$backup_number" =~ ^[0-9]+$ ]] && [[ "$backup_number" -ge 1 ]] && [[ "$backup_number" -le "${#backup_files[@]}" ]]; then
|
||||||
|
selected_backup="${backup_files[$((backup_number-1))]}"
|
||||||
|
restore_database "$selected_backup"
|
||||||
|
else
|
||||||
|
echo "❌ Invalid backup number!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
read -p "Enter backup file path: " custom_backup
|
||||||
|
restore_database "$custom_backup"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
echo "👋 Goodbye!"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ Invalid option!"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "🎉 Restore process completed!"
|
||||||
@@ -246,6 +246,8 @@ Route::group(['middleware' => 'auth'], function() {
|
|||||||
Route::get('create', 'create')->name('create');
|
Route::get('create', 'create')->name('create');
|
||||||
Route::post('/', 'store')->name('store');
|
Route::post('/', 'store')->name('store');
|
||||||
Route::get('get-product-stock', 'getProductStock')->name('get-product-stock');
|
Route::get('get-product-stock', 'getProductStock')->name('get-product-stock');
|
||||||
|
Route::get('get-pending-mutations', 'getPendingMutations')->name('get-pending-mutations');
|
||||||
|
Route::get('{mutation}/get-detail', 'getDetail')->name('get-detail');
|
||||||
Route::get('{mutation}', 'show')->name('show');
|
Route::get('{mutation}', 'show')->name('show');
|
||||||
Route::get('{mutation}/edit', 'edit')->name('edit');
|
Route::get('{mutation}/edit', 'edit')->name('edit');
|
||||||
Route::get('{mutation}/details', 'getDetails')->name('details');
|
Route::get('{mutation}/details', 'getDetails')->name('details');
|
||||||
|
|||||||
Reference in New Issue
Block a user