add backup file and autobackup code, partial update mutations receive on transation page

This commit is contained in:
2025-06-12 18:09:13 +07:00
parent 58578532cc
commit b04b8f88cb
9 changed files with 1276 additions and 21 deletions

356
BACKUP_README.md Normal file
View 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**

View File

@@ -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
View 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
View 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!"

Binary file not shown.

40
ckb.sql

File diff suppressed because one or more lines are too long

View File

@@ -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">&times;</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
View 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!"

View File

@@ -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');