Add laundry open/close times and status fields
Features: - Laundry: Add jam_buka, jam_tutup, status (buka/tutup) fields to database - Web forms: Add opening/closing times and status dropdown to create/edit views - Controller: Validate and save new fields in store() and update() - Mobile: Parse time strings to remove seconds, display status and hours - Mobile: Add cache clearing to auto-refresh image data from server - Mobile: Fix photo priority (prefer 'foto' field over galeri entries) Changes: - spk_kontrakan/app/Models/Laundry.php: Update fillable - spk_kontrakan/app/Http/Controllers/LaundryController.php: Add validation and storage - spk_kontrakan/resources/views/Laundry/create.blade.php: Add form fields - spk_kontrakan/resources/views/Laundry/edit.blade.php: Add form fields - spk_mobile/lib/models/laundry.dart: Time parsing, photo priority fix - spk_mobile/lib/models/kontrakan.dart: Photo priority fix - spk_mobile/pubspec.yaml: Add flutter_cache_manager - Mobile screens: Add cache clearing on data refresh
This commit is contained in:
parent
786743231c
commit
73e8f197f2
|
|
@ -302,6 +302,9 @@ public function store(Request $request)
|
|||
'latitude' => 'required|numeric|between:-90,90',
|
||||
'longitude' => 'required|numeric|between:-180,180',
|
||||
'fasilitas' => 'nullable|string|max:1000',
|
||||
'jam_buka' => 'required|date_format:H:i',
|
||||
'jam_tutup' => 'required|date_format:H:i',
|
||||
'status' => 'required|in:buka,tutup',
|
||||
'foto' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
|
||||
|
||||
// Validasi Layanan
|
||||
|
|
@ -395,6 +398,9 @@ public function store(Request $request)
|
|||
'longitude' => $validated['longitude'],
|
||||
'jarak' => round($jarakKm * 1000), // Simpan dalam meter (seperti kontrakan)
|
||||
'fasilitas' => $request->fasilitas,
|
||||
'jam_buka' => $validated['jam_buka'],
|
||||
'jam_tutup' => $validated['jam_tutup'],
|
||||
'status' => $validated['status'],
|
||||
'foto' => $filename,
|
||||
]);
|
||||
|
||||
|
|
@ -491,6 +497,9 @@ public function update(Request $request, Laundry $laundry)
|
|||
'latitude' => 'required|numeric|between:-90,90',
|
||||
'longitude' => 'required|numeric|between:-180,180',
|
||||
'fasilitas' => 'nullable|string|max:1000',
|
||||
'jam_buka' => 'required|date_format:H:i',
|
||||
'jam_tutup' => 'required|date_format:H:i',
|
||||
'status' => 'required|in:buka,tutup',
|
||||
'foto' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
|
||||
'hapus_foto' => 'nullable|in:0,1',
|
||||
|
||||
|
|
@ -604,6 +613,9 @@ public function update(Request $request, Laundry $laundry)
|
|||
'longitude' => $validated['longitude'],
|
||||
'jarak' => round($jarakKm * 1000), // Simpan dalam meter (seperti kontrakan)
|
||||
'fasilitas' => $request->fasilitas,
|
||||
'jam_buka' => $validated['jam_buka'],
|
||||
'jam_tutup' => $validated['jam_tutup'],
|
||||
'status' => $validated['status'],
|
||||
'foto' => $filename,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ class Laundry extends Model
|
|||
'longitude',
|
||||
'jarak',
|
||||
'fasilitas',
|
||||
'jam_buka',
|
||||
'jam_tutup',
|
||||
'status',
|
||||
'foto',
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -225,6 +225,67 @@ class="form-control border-start-0 @error('fasilitas') is-invalid @enderror"
|
|||
</div>
|
||||
<small class="text-muted">Fasilitas yang disediakan laundry ini</small>
|
||||
</div>
|
||||
|
||||
<!-- Status Operasional -->
|
||||
<div class="col-md-4">
|
||||
<label for="status" class="form-label fw-semibold">
|
||||
Status Operasional <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select name="status" id="status" class="form-select @error('status') is-invalid @enderror" required>
|
||||
<option value="buka" {{ old('status', 'buka') == 'buka' ? 'selected' : '' }}>🟢 Buka</option>
|
||||
<option value="tutup" {{ old('status') == 'tutup' ? 'selected' : '' }}>🔴 Tutup</option>
|
||||
</select>
|
||||
@error('status')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
<small class="text-muted">Status laundry saat ini</small>
|
||||
</div>
|
||||
|
||||
<!-- Jam Buka -->
|
||||
<div class="col-md-4">
|
||||
<label for="jam_buka" class="form-label fw-semibold">
|
||||
Jam Buka <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<i class="bi bi-clock-history text-success"></i>
|
||||
</span>
|
||||
<input
|
||||
type="time"
|
||||
name="jam_buka"
|
||||
id="jam_buka"
|
||||
class="form-control border-start-0 @error('jam_buka') is-invalid @enderror"
|
||||
value="{{ old('jam_buka', '08:00') }}"
|
||||
required
|
||||
>
|
||||
@error('jam_buka')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jam Tutup -->
|
||||
<div class="col-md-4">
|
||||
<label for="jam_tutup" class="form-label fw-semibold">
|
||||
Jam Tutup <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<i class="bi bi-clock text-danger"></i>
|
||||
</span>
|
||||
<input
|
||||
type="time"
|
||||
name="jam_tutup"
|
||||
id="jam_tutup"
|
||||
class="form-control border-start-0 @error('jam_tutup') is-invalid @enderror"
|
||||
value="{{ old('jam_tutup', '20:00') }}"
|
||||
required
|
||||
>
|
||||
@error('jam_tutup')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -222,6 +222,67 @@ class="form-control border-start-0 @error('fasilitas') is-invalid @enderror"
|
|||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Operasional -->
|
||||
<div class="col-md-4">
|
||||
<label for="status" class="form-label fw-semibold">
|
||||
Status Operasional <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select name="status" id="status" class="form-select @error('status') is-invalid @enderror" required>
|
||||
<option value="buka" {{ old('status', $laundry->status) == 'buka' ? 'selected' : '' }}>🟢 Buka</option>
|
||||
<option value="tutup" {{ old('status', $laundry->status) == 'tutup' ? 'selected' : '' }}>🔴 Tutup</option>
|
||||
</select>
|
||||
@error('status')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
<small class="text-muted">Status laundry saat ini</small>
|
||||
</div>
|
||||
|
||||
<!-- Jam Buka -->
|
||||
<div class="col-md-4">
|
||||
<label for="jam_buka" class="form-label fw-semibold">
|
||||
Jam Buka <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<i class="bi bi-clock-history text-success"></i>
|
||||
</span>
|
||||
<input
|
||||
type="time"
|
||||
name="jam_buka"
|
||||
id="jam_buka"
|
||||
class="form-control border-start-0 @error('jam_buka') is-invalid @enderror"
|
||||
value="{{ old('jam_buka', \Carbon\Carbon::parse($laundry->jam_buka)->format('H:i')) }}"
|
||||
required
|
||||
>
|
||||
@error('jam_buka')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jam Tutup -->
|
||||
<div class="col-md-4">
|
||||
<label for="jam_tutup" class="form-label fw-semibold">
|
||||
Jam Tutup <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<i class="bi bi-clock text-danger"></i>
|
||||
</span>
|
||||
<input
|
||||
type="time"
|
||||
name="jam_tutup"
|
||||
id="jam_tutup"
|
||||
class="form-control border-start-0 @error('jam_tutup') is-invalid @enderror"
|
||||
value="{{ old('jam_tutup', \Carbon\Carbon::parse($laundry->jam_tutup)->format('H:i')) }}"
|
||||
required
|
||||
>
|
||||
@error('jam_tutup')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -756,7 +756,7 @@
|
|||
@foreach($recentLaundry as $l)
|
||||
<div class="list-group-item px-0 py-2">
|
||||
<div class="d-flex w-100 justify-content-between align-items-start">
|
||||
<div class="flex-grow-1 me-2">
|
||||
<div class="flex-grow-1 me-2">
|
||||
<h6 class="mb-1 fw-semibold" style="font-size: 0.875rem;">{{ $l->nama }}</h6>
|
||||
<p class="mb-0 text-muted" style="font-size: 0.75rem;">
|
||||
<i class="bi bi-geo-alt me-1"></i>{{ Str::limit($l->alamat ?? '-', 25) }}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ class AppConfig {
|
|||
|
||||
// CATATAN: Untuk real device, ganti IP sesuai dengan IP komputer Anda
|
||||
// Cek IP dengan: ipconfig (di Windows) atau ifconfig (di Linux/Mac)
|
||||
static const String baseUrl = 'http://10.215.176.99:8000/api';
|
||||
static const String storageUrl = 'http://10.215.176.99:8000/storage';
|
||||
static const String baseUrl = 'http://192.168.18.16:8000/api';
|
||||
static const String storageUrl = 'http://192.168.18.16:8000/storage';
|
||||
|
||||
// Timeouts
|
||||
static const Duration connectionTimeout = Duration(seconds: 10);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class Kontrakan {
|
|||
final String? deskripsi;
|
||||
final String status;
|
||||
final String? fotoUtama;
|
||||
final String? foto; // Field foto dari API
|
||||
final String? noWhatsapp;
|
||||
final List<Galeri> galeri;
|
||||
final double? avgRating;
|
||||
|
|
@ -27,6 +28,7 @@ class Kontrakan {
|
|||
this.deskripsi,
|
||||
required this.status,
|
||||
this.fotoUtama,
|
||||
this.foto,
|
||||
this.noWhatsapp,
|
||||
this.galeri = const [],
|
||||
this.avgRating,
|
||||
|
|
@ -45,6 +47,28 @@ class Kontrakan {
|
|||
jarak = (double.tryParse(json['jarak'].toString()) ?? 0) / 1000;
|
||||
}
|
||||
|
||||
// Get foto from API and build gallery list
|
||||
final List<Galeri> galleryList = [];
|
||||
|
||||
// Parse galeri array if exists
|
||||
if (json['galeri'] != null && (json['galeri'] as List).isNotEmpty) {
|
||||
galleryList.addAll(
|
||||
(json['galeri'] as List).map((g) => Galeri.fromJson(g)).toList()
|
||||
);
|
||||
}
|
||||
|
||||
// If gallery is empty but foto field exists, create a galeri item from it
|
||||
if (galleryList.isEmpty && json['foto'] != null && json['foto'].toString().isNotEmpty) {
|
||||
galleryList.add(
|
||||
Galeri(
|
||||
id: json['id'] ?? 0,
|
||||
foto: json['foto'].toString(),
|
||||
isPrimary: true,
|
||||
urutan: 0,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Kontrakan(
|
||||
id: json['id'] ?? 0,
|
||||
nama: json['nama'] ?? '',
|
||||
|
|
@ -56,10 +80,9 @@ class Kontrakan {
|
|||
deskripsi: json['deskripsi'],
|
||||
status: json['status'] ?? 'tersedia',
|
||||
fotoUtama: json['foto_utama'],
|
||||
foto: json['foto'], // Store raw foto field
|
||||
noWhatsapp: json['no_whatsapp'],
|
||||
galeri: json['galeri'] != null
|
||||
? (json['galeri'] as List).map((g) => Galeri.fromJson(g)).toList()
|
||||
: [],
|
||||
galeri: galleryList,
|
||||
avgRating: json['avg_rating'] != null
|
||||
? double.tryParse(json['avg_rating'].toString())
|
||||
: null,
|
||||
|
|
@ -77,15 +100,32 @@ class Kontrakan {
|
|||
bool get isAvailable => status == 'available' || status == 'tersedia';
|
||||
|
||||
// Get primary photo URL
|
||||
// Prioritize 'foto' field (Admin-set primary photo) over galeri entries,
|
||||
// because galeri items may reference files that have been deleted/replaced.
|
||||
String get primaryPhoto {
|
||||
// 1. Prefer the explicit 'foto' field (most up-to-date primary photo)
|
||||
if (foto != null && foto!.isNotEmpty) {
|
||||
if (foto!.startsWith('http')) {
|
||||
return foto!;
|
||||
}
|
||||
const String baseUrl = 'http://192.168.18.16:8000';
|
||||
return '$baseUrl/uploads/Kontrakan/$foto';
|
||||
}
|
||||
// 2. Fall back to galeri items if foto is not set
|
||||
if (galeri.isNotEmpty) {
|
||||
final primary = galeri.firstWhere(
|
||||
(g) => g.isPrimary,
|
||||
orElse: () => galeri.first,
|
||||
);
|
||||
return primary.foto;
|
||||
if (primary.foto.isNotEmpty && primary.foto.startsWith('http')) {
|
||||
return primary.foto;
|
||||
}
|
||||
if (primary.foto.isNotEmpty) {
|
||||
const String baseUrl = 'http://192.168.18.16:8000';
|
||||
return '$baseUrl/uploads/Kontrakan/${primary.foto}';
|
||||
}
|
||||
}
|
||||
return fotoUtama ?? 'https://via.placeholder.com/300';
|
||||
return 'https://via.placeholder.com/300';
|
||||
}
|
||||
|
||||
// Format harga dengan Rupiah
|
||||
|
|
@ -120,4 +160,15 @@ class Galeri {
|
|||
urutan: json['urutan'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
// Get full photo URL
|
||||
String get photoUrl {
|
||||
// Check if it's already a full URL
|
||||
if (foto.startsWith('http')) {
|
||||
return foto;
|
||||
}
|
||||
// Build full URL from uploads
|
||||
const String baseUrl = 'http://192.168.18.16:8000';
|
||||
return '$baseUrl/uploads/Kontrakan/$foto';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class Laundry {
|
|||
final int waktuProses; // dalam jam
|
||||
final String? deskripsi;
|
||||
final String? fotoUtama;
|
||||
final String? foto; // Field foto dari API
|
||||
final List<GaleriLaundry> galeri;
|
||||
final double? avgRating;
|
||||
final int? totalReviews;
|
||||
|
|
@ -31,6 +32,7 @@ class Laundry {
|
|||
required this.waktuProses,
|
||||
this.deskripsi,
|
||||
this.fotoUtama,
|
||||
this.foto,
|
||||
this.galeri = const [],
|
||||
this.avgRating,
|
||||
this.totalReviews,
|
||||
|
|
@ -58,6 +60,28 @@ class Laundry {
|
|||
double.tryParse(layananList.first['harga']?.toString() ?? '0') ?? 0;
|
||||
}
|
||||
|
||||
// Get foto from API and build gallery list
|
||||
final List<GaleriLaundry> galleryList = [];
|
||||
|
||||
// Parse galeri array if exists
|
||||
if (json['galeri'] != null && (json['galeri'] as List).isNotEmpty) {
|
||||
galleryList.addAll(
|
||||
(json['galeri'] as List).map((g) => GaleriLaundry.fromJson(g)).toList()
|
||||
);
|
||||
}
|
||||
|
||||
// If gallery is empty but foto field exists, create a galeri item from it
|
||||
if (galleryList.isEmpty && json['foto'] != null && json['foto'].toString().isNotEmpty) {
|
||||
galleryList.add(
|
||||
GaleriLaundry(
|
||||
id: json['id'] ?? 0,
|
||||
foto: json['foto'].toString(),
|
||||
isPrimary: true,
|
||||
urutan: 0,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Laundry(
|
||||
id: json['id'] ?? 0,
|
||||
nama: json['nama'] ?? '',
|
||||
|
|
@ -70,17 +94,14 @@ class Laundry {
|
|||
? double.tryParse(json['longitude'].toString())
|
||||
: null,
|
||||
jarakKampus: jarak,
|
||||
jamBuka: json['jam_buka'] ?? '08:00',
|
||||
jamTutup: json['jam_tutup'] ?? '17:00',
|
||||
jamBuka: _parseTime(json['jam_buka'], '08:00'),
|
||||
jamTutup: _parseTime(json['jam_tutup'], '20:00'),
|
||||
hargaPerKg: harga,
|
||||
waktuProses: json['waktu_proses'] ?? 24,
|
||||
deskripsi: json['deskripsi'],
|
||||
fotoUtama: json['foto_utama'],
|
||||
galeri: json['galeri'] != null
|
||||
? (json['galeri'] as List)
|
||||
.map((g) => GaleriLaundry.fromJson(g))
|
||||
.toList()
|
||||
: [],
|
||||
foto: json['foto'], // Store raw foto field
|
||||
galeri: galleryList,
|
||||
avgRating: json['avg_rating'] != null
|
||||
? double.tryParse(json['avg_rating'].toString())
|
||||
: null,
|
||||
|
|
@ -89,16 +110,41 @@ class Laundry {
|
|||
);
|
||||
}
|
||||
|
||||
// Parse time string, stripping seconds if present (e.g. "08:00:00" -> "08:00")
|
||||
static String _parseTime(dynamic value, String fallback) {
|
||||
if (value == null) return fallback;
|
||||
final str = value.toString();
|
||||
if (str.length >= 5) return str.substring(0, 5);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// Get primary photo URL
|
||||
// Prioritize 'foto' field (Admin-set primary photo) over galeri entries,
|
||||
// because galeri items may reference files that have been deleted/replaced.
|
||||
String get primaryPhoto {
|
||||
// 1. Prefer the explicit 'foto' field (most up-to-date primary photo)
|
||||
if (foto != null && foto!.isNotEmpty) {
|
||||
if (foto!.startsWith('http')) {
|
||||
return foto!;
|
||||
}
|
||||
const String baseUrl = 'http://192.168.18.16:8000';
|
||||
return '$baseUrl/uploads/Laundry/$foto';
|
||||
}
|
||||
// 2. Fall back to galeri items if foto is not set
|
||||
if (galeri.isNotEmpty) {
|
||||
final primary = galeri.firstWhere(
|
||||
(g) => g.isPrimary,
|
||||
orElse: () => galeri.first,
|
||||
);
|
||||
return primary.foto;
|
||||
if (primary.foto.isNotEmpty && primary.foto.startsWith('http')) {
|
||||
return primary.foto;
|
||||
}
|
||||
if (primary.foto.isNotEmpty) {
|
||||
const String baseUrl = 'http://192.168.18.16:8000';
|
||||
return '$baseUrl/uploads/Laundry/${primary.foto}';
|
||||
}
|
||||
}
|
||||
return fotoUtama ?? 'https://via.placeholder.com/300';
|
||||
return 'https://via.placeholder.com/300';
|
||||
}
|
||||
|
||||
// Format harga dengan Rupiah
|
||||
|
|
@ -155,4 +201,15 @@ class GaleriLaundry {
|
|||
urutan: json['urutan'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
// Get full photo URL
|
||||
String get photoUrl {
|
||||
// Check if it's already a full URL
|
||||
if (foto.startsWith('http')) {
|
||||
return foto;
|
||||
}
|
||||
// Build full URL from uploads
|
||||
const String baseUrl = 'http://192.168.18.16:8000';
|
||||
return '$baseUrl/uploads/Laundry/$foto';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import '../models/kontrakan.dart';
|
||||
import '../models/laundry.dart';
|
||||
import '../services/favorite_service.dart';
|
||||
|
|
@ -37,6 +38,10 @@ class _FavoritesScreenState extends State<FavoritesScreen>
|
|||
|
||||
Future<void> _loadFavorites() async {
|
||||
setState(() => _isLoading = true);
|
||||
// Clear image cache agar foto terbaru selalu dimuat
|
||||
await DefaultCacheManager().emptyCache();
|
||||
PaintingBinding.instance.imageCache.clear();
|
||||
PaintingBinding.instance.imageCache.clearLiveImages();
|
||||
try {
|
||||
final result = await _favoriteService.getFavoritesWithModels();
|
||||
if (mounted) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'kontrakan_detail_screen.dart';
|
|||
import 'laundry_detail_screen.dart';
|
||||
import '../login.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
|
||||
class ImprovedHomeScreen extends StatefulWidget {
|
||||
const ImprovedHomeScreen({super.key});
|
||||
|
|
@ -43,6 +44,10 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
|
|||
|
||||
Future<void> _loadData() async {
|
||||
setState(() => _isLoading = true);
|
||||
// Clear image cache agar foto terbaru dari server selalu dimuat
|
||||
await DefaultCacheManager().emptyCache();
|
||||
PaintingBinding.instance.imageCache.clear();
|
||||
PaintingBinding.instance.imageCache.clearLiveImages();
|
||||
await Future.wait([_loadKontrakan(), _loadLaundry(), _loadFavoriteIds()]);
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
|
|||
itemBuilder: (context, index) {
|
||||
final imageUrl = widget.kontrakan.galeri.isEmpty
|
||||
? widget.kontrakan.primaryPhoto
|
||||
: widget.kontrakan.galeri[index].foto;
|
||||
: widget.kontrakan.galeri[index].photoUrl;
|
||||
|
||||
return CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:http/http.dart' as http;
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../models/kontrakan.dart';
|
||||
import '../models/laundry.dart';
|
||||
|
|
@ -173,6 +174,11 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
|
|||
_noData = false;
|
||||
});
|
||||
|
||||
// Clear image cache agar foto terbaru selalu dimuat
|
||||
await DefaultCacheManager().emptyCache();
|
||||
PaintingBinding.instance.imageCache.clear();
|
||||
PaintingBinding.instance.imageCache.clearLiveImages();
|
||||
|
||||
final endpoint = widget.category == 'kontrakan'
|
||||
? '/saw/calculate/kontrakan'
|
||||
: '/saw/calculate/laundry';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import '../models/kontrakan.dart';
|
||||
import '../models/laundry.dart';
|
||||
import '../services/kontrakan_service.dart';
|
||||
|
|
@ -49,6 +50,10 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
|
||||
Future<void> _loadData() async {
|
||||
setState(() => _isLoading = true);
|
||||
// Clear image cache agar data terbaru dari server selalu dimuat
|
||||
await DefaultCacheManager().emptyCache();
|
||||
PaintingBinding.instance.imageCache.clear();
|
||||
PaintingBinding.instance.imageCache.clearLiveImages();
|
||||
final kontrakan = await _kontrakanService.getKontrakan();
|
||||
final laundry = await _laundryService.getLaundry();
|
||||
setState(() {
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ packages:
|
|||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_cache_manager:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ dependencies:
|
|||
|
||||
# Image picker & display
|
||||
cached_network_image: ^3.3.1
|
||||
flutter_cache_manager: ^3.3.1
|
||||
image_picker: ^1.1.2
|
||||
|
||||
# State management
|
||||
|
|
|
|||
Loading…
Reference in New Issue