fix: resolve existing photos preview in Dropzone Lahan&Tanaman
This commit is contained in:
@@ -97,11 +97,12 @@ class LahanController extends Controller
|
||||
'nama' => ['required', 'string', 'max:255'],
|
||||
'status_kepemilikan' => ['required', new Enum(StatusLahan::class)],
|
||||
'desa_kelurahan_id' => ['nullable', 'numeric', 'exists:master_desa_kelurahan,id'],
|
||||
'path' => ['nullable', 'array'],
|
||||
'path.*' => ['array'],
|
||||
'path' => ['nullable', 'string'],
|
||||
'luas_lahan' => ['nullable', 'numeric'],
|
||||
'uid' => ['required', 'string'],
|
||||
'profile_id' => $profileRule,
|
||||
'foto' => ['nullable', 'array'],
|
||||
'foto.*' => ['image', 'mimes:jpeg,png,jpg', 'max:5120'], // Max 5MB
|
||||
]);
|
||||
|
||||
$targetProfileId = $user->role === 'fasilitator'
|
||||
@@ -120,10 +121,22 @@ class LahanController extends Controller
|
||||
? $profile->desa_kelurahan_id
|
||||
: ($validated['desa_kelurahan_id'] ?? $profile->desa_kelurahan_id);
|
||||
|
||||
$fotoPaths = [];
|
||||
if ($request->hasFile('foto')) {
|
||||
foreach ($request->file('foto') as $file) {
|
||||
$fotoPaths[] = $file->store('foto_lahan', 'public');
|
||||
}
|
||||
}
|
||||
|
||||
$lahan = Lahan::create([
|
||||
...$validated,
|
||||
'profile_id' => $profile->id,
|
||||
'nama' => $validated['nama'],
|
||||
'status_kepemilikan' => $validated['status_kepemilikan'],
|
||||
'desa_kelurahan_id' => $desaKelurahanId,
|
||||
'path' => $validated['path'] ?? null,
|
||||
'luas_lahan' => $validated['luas_lahan'] ?? null,
|
||||
'uid' => $validated['uid'],
|
||||
'profile_id' => $profile->id,
|
||||
'foto' => $fotoPaths,
|
||||
]);
|
||||
|
||||
return (new LahanResource($lahan))
|
||||
@@ -173,9 +186,26 @@ class LahanController extends Controller
|
||||
'path' => ['nullable', 'array'],
|
||||
'path.*' => ['array'],
|
||||
'luas_lahan' => ['nullable', 'numeric'],
|
||||
'foto' => ['nullable', 'array'],
|
||||
'foto.*' => ['image', 'mimes:jpeg,png,jpg', 'max:5120'],
|
||||
]);
|
||||
|
||||
$lahan->update($validated);
|
||||
$fotoPaths = $lahan->foto ?? [];
|
||||
|
||||
if ($request->hasFile('foto')) {
|
||||
foreach ($request->file('foto') as $file) {
|
||||
$fotoPaths[] = $file->store('foto_lahan', 'public');
|
||||
}
|
||||
}
|
||||
|
||||
$lahan->update([
|
||||
'nama' => $validated['nama'],
|
||||
'status_kepemilikan' => $validated['status_kepemilikan'],
|
||||
'desa_kelurahan_id' => $validated['desa_kelurahan_id'] ?? $lahan->desa_kelurahan_id,
|
||||
'path' => $validated['path'] ?? null,
|
||||
'luas_lahan' => $validated['luas_lahan'] ?? null,
|
||||
'foto' => $fotoPaths,
|
||||
]);
|
||||
|
||||
return (new LahanResource($lahan))->response();
|
||||
}
|
||||
|
||||
@@ -256,9 +256,28 @@ class PetaniLahanController extends Controller
|
||||
$tanamanCount = Tanaman::where('lahan_id', $lahan->id)->count();
|
||||
$lahanCount = Lahan::where('petani_id', $petaniId)->count();
|
||||
|
||||
$fotoLama = $lahan->foto ?? [];
|
||||
if (is_string($fotoLama)) {
|
||||
$fotoLama = json_decode($fotoLama, true);
|
||||
}
|
||||
if (!is_array($fotoLama)) {
|
||||
$fotoLama = [];
|
||||
}
|
||||
|
||||
if ($request->hasFile('foto')) {
|
||||
foreach ($request->file('foto') as $file) {
|
||||
$path = $file->store('lahan', 'public');
|
||||
$fotoLama[] = [
|
||||
'path' => $path,
|
||||
'file' => $file->getClientOriginalName(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
unset($validated['foto']);
|
||||
|
||||
$lahan->update([...$validated,
|
||||
//'nama' => 'Lahan ' . $komoditas->nama . ' #' . ($lahanCount + 1),
|
||||
//'petani_id' => $petaniId,
|
||||
'foto' => count($fotoLama) > 0 ? $fotoLama : null,
|
||||
'updated_by' => auth()->id()]
|
||||
);
|
||||
|
||||
@@ -293,7 +312,15 @@ class PetaniLahanController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
return redirect()->route('petani.lahan-tanaman.index', $petaniId)->with('success', 'Lahan berhasil ditambahkan.');
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'redirect' => route('petani.lahan-tanaman.index', $petaniId),
|
||||
'message' => 'Lahan berhasil diupdate.'
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('petani.lahan-tanaman.index', $petaniId)->with('success', 'Lahan berhasil diperbarui.');
|
||||
}
|
||||
|
||||
public function destroy(string $id)
|
||||
|
||||
@@ -389,7 +389,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<input type="file" id="foto-input" name="foto[]" multiple hidden>
|
||||
<input type="file" id="foto-input" multiple hidden>
|
||||
@error('file')<div class="error invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
</div>
|
||||
@@ -759,8 +759,6 @@
|
||||
tempInput.click();
|
||||
}
|
||||
});
|
||||
// -----------------------------------
|
||||
// FORM
|
||||
|
||||
$('#petani-lahan-tanaman').validate({
|
||||
rules: {
|
||||
@@ -805,21 +803,44 @@
|
||||
submitHandler: function (form) {
|
||||
const formData = new FormData(form);
|
||||
|
||||
const dz = $("#dropzone-foto")[0].dropzone;
|
||||
|
||||
dz.getAcceptedFiles().forEach(function(file) {
|
||||
if (myDropzone) {
|
||||
myDropzone.files.forEach(function(file) {
|
||||
formData.append("foto[]", file);
|
||||
});
|
||||
}
|
||||
|
||||
let submitBtn = $(form).find('button[type="submit"]');
|
||||
let originalText = submitBtn.html();
|
||||
submitBtn.prop('disabled', true).html('<i class="ti tabler-loader fa-spin me-2"></i> Menyimpan...');
|
||||
|
||||
$.ajax({
|
||||
url: form.action,
|
||||
url: $(form).attr('action'),
|
||||
type: "POST",
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': $('input[name="_token"]').val(),
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
success: function(res) {
|
||||
if (res.redirect) {
|
||||
window.location.href = res.redirect;
|
||||
alert('Data lahan berhasil ditambahkan!');
|
||||
window.location.href = "{{ route('petani.index') }}";
|
||||
},
|
||||
error: function(xhr) {
|
||||
submitBtn.prop('disabled', false).html(originalText);
|
||||
|
||||
if (xhr.status === 422) {
|
||||
let errors = xhr.responseJSON.errors;
|
||||
let errorMsg = "";
|
||||
$.each(errors, function(key, val) {
|
||||
errorMsg += val[0] + "\n";
|
||||
});
|
||||
alert("Penyimpanan Gagal:\n\n" + errorMsg);
|
||||
} else {
|
||||
let errorDetail = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : xhr.statusText;
|
||||
alert("Error " + xhr.status + ": " + errorDetail);
|
||||
console.error(xhr.responseText);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@push('style')
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" type="text/css" />
|
||||
|
||||
<style>
|
||||
#map {
|
||||
@@ -11,6 +12,28 @@
|
||||
background-color: #f8f9fa;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dropzone-custom-container { display: flex; flex-wrap: wrap; gap: 15px; padding: 15px; border: 2px dashed #ced4da; border-radius: 12px; background-color: #f8f9fa; min-height: 160px; align-items: center; }
|
||||
.btn-tambah-foto { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 140px; height: 160px; border: 2px dashed #a8b1ba; border-radius: 12px; background: #fff; color: #6c757d; cursor: pointer; transition: 0.2s; order: 999; }
|
||||
.btn-tambah-foto:hover { background: #e9ecef; border-color: #6c757d; color: #495057; }
|
||||
.dropzone-custom-container .dz-preview { position: relative; background: #fff; border-radius: 12px; box-shadow: 0 4px 10px rgba(0,0,0,0.08); border: 1px solid #e2e8f0; width: 140px; display: flex; flex-direction: column; }
|
||||
.dropzone-custom-container .dz-image { width: 100%; height: 110px; background: #f1f5f9; cursor: zoom-in; display: flex; align-items: center; justify-content: center; border-radius: 12px 12px 0 0; overflow: hidden; }
|
||||
.dropzone-custom-container .dz-image img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.dropzone-custom-container .dz-details { padding: 8px; text-align: center; background: #fff; border-radius: 0 0 12px 12px; border-top: 1px solid #e2e8f0; }
|
||||
.dropzone-custom-container .dz-filename { font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #495057; }
|
||||
.dropzone-custom-container .dz-size { font-size: 10px; color: #adb5bd; }
|
||||
.dropzone-custom-container .dz-control-panel { position: absolute; top: -8px; right: -8px; display: flex; gap: 5px; z-index: 20; }
|
||||
.dropzone-custom-container .dz-btn-action { display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 50%; color: #fff !important; text-decoration: none !important; box-shadow: 0 2px 5px rgba(0,0,0,0.2); cursor: pointer; font-size: 14px; }
|
||||
.dropzone-custom-container .btn-edit { background: #ffc107; color: #000 !important; }
|
||||
.dropzone-custom-container .btn-edit:hover { background: #7367f0; }
|
||||
.dropzone-custom-container .btn-delete { background: #dc3545; }
|
||||
.dropzone-custom-container .btn-delete:hover { background: #c82333; }
|
||||
.dropzone-custom-container .dz-error-message, .dropzone-custom-container .dz-success-mark, .dropzone-custom-container .dz-error-mark, .dropzone-custom-container .progress { display: none !important; }
|
||||
.foto-lightbox { display: none; position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; background: rgba(0, 0, 0, 0.85) !important; z-index: 99999 !important; align-items: center !important; justify-content: center !important; padding: 30px !important; box-sizing: border-box !important; }
|
||||
.foto-lightbox.active { display: flex !important; }
|
||||
.foto-lightbox img { max-width: 90vw !important; max-height: 85vh !important; object-fit: contain !important; border-radius: 8px !important; box-shadow: 0 10px 40px rgba(0,0,0,0.5) !important; }
|
||||
.foto-lightbox-close { position: absolute !important; top: 20px !important; right: 30px !important; color: #fff !important; font-size: 40px !important; font-weight: bold !important; cursor: pointer !important; z-index: 100000 !important; }
|
||||
.foto-lightbox-close:hover { color: #dc3545 !important; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@@ -225,8 +248,7 @@
|
||||
<div class="col-md-12 mb-3 form-control-validation">
|
||||
<label class="form-label">Raw Data Koordinat Lahan (Auto-generated JSON) <span class="text-danger">*</span></label>
|
||||
<textarea name="path" id="path" class="form-control @error('path') is-invalid @enderror" rows="4" readonly
|
||||
placeholder="Koordinat akan terisi otomatis setelah Anda menggambar polygon di peta...">{{ old('path', $lahanSelected->path) }}</textarea>
|
||||
<small class="text-muted">Gunakan tombol edit (pensil) atau hapus (tong sampah) di peta untuk mengubah bentuk lahan.</small>
|
||||
placeholder="Koordinat akan terisi otomatis setelah Anda menggambar polygon di peta...">{{ old('path', is_string($lahanSelected->path) ? $lahanSelected->path : json_encode($lahanSelected->path)) }}</textarea>
|
||||
@error('path')<div class="error invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,6 +265,24 @@
|
||||
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-3 form-control-validation">
|
||||
<h6 class="mb-5">Foto</h6>
|
||||
<div id="dropzone-container" class="dropzone-custom-container">
|
||||
<div id="btn-tambah-foto" class="btn-tambah-foto">
|
||||
<i class="ti tabler-plus" style="font-size: 32px; margin-bottom: 8px;"></i>
|
||||
<span style="font-size: 13px; font-weight: 600;">Tambah Foto</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="deleted-photos-container"></div>
|
||||
|
||||
<input type="file" id="foto-input" multiple hidden>
|
||||
@error('file')<div class="error invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 text-end">
|
||||
<a href="{{ route('petani.index') }}" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
|
||||
@@ -358,6 +398,7 @@
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://unpkg.com/dropzone@5/dist/min/dropzone.min.js"></script>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
||||
|
||||
@@ -546,7 +587,7 @@
|
||||
let regionBoundaryLayer = L.featureGroup().addTo(map);
|
||||
|
||||
function focusToRegionBoundary(selectElement, shouldZoom = true) {
|
||||
regionBoundaryLayer.clearLayers(); // Bersihkan garis wilayah sebelumnya
|
||||
regionBoundaryLayer.clearLayers();
|
||||
|
||||
let selectedOption = selectElement.options[selectElement.selectedIndex];
|
||||
if (!selectedOption || !selectedOption.hasAttribute('data-path')) return;
|
||||
@@ -561,7 +602,7 @@
|
||||
|
||||
function formatCoordinates(coords) {
|
||||
if (coords.length === 2 && typeof coords[0] === 'number') {
|
||||
return [coords[0], coords[1]]; // JANGAN DIBALIK
|
||||
return [coords[0], coords[1]];
|
||||
}
|
||||
return coords.map(formatCoordinates);
|
||||
}
|
||||
@@ -637,10 +678,8 @@
|
||||
|
||||
map.on(L.Draw.Event.CREATED, function (e) {
|
||||
let layer = e.layer;
|
||||
|
||||
drawnItems.clearLayers();
|
||||
drawnItems.addLayer(layer);
|
||||
|
||||
updatePathTextarea(layer);
|
||||
});
|
||||
|
||||
@@ -660,16 +699,143 @@
|
||||
return [latlng.lng, latlng.lat];
|
||||
});
|
||||
document.getElementById('path').value = JSON.stringify(pathData);
|
||||
|
||||
let area = L.GeometryUtil.geodesicArea(latlngs);
|
||||
|
||||
console.log(area);
|
||||
|
||||
$('#luas_lahan').val(area.toFixed(2));
|
||||
}
|
||||
|
||||
// FORM
|
||||
const previewTemplate = `
|
||||
<div class="dz-preview dz-file-preview">
|
||||
<div class="dz-control-panel">
|
||||
<a href="javascript:void(0);" class="dz-btn-action btn-edit" title="Ganti Foto">
|
||||
<i class="ti tabler-pencil"></i>
|
||||
</a>
|
||||
<a href="javascript:void(0);" class="dz-btn-action btn-delete" data-dz-remove title="Hapus Foto">
|
||||
<i class="ti tabler-x"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="dz-image"><img data-dz-thumbnail /></div>
|
||||
<div class="dz-details">
|
||||
<div class="dz-filename"><span data-dz-name></span></div>
|
||||
<div class="dz-size"><span data-dz-size></span></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
Dropzone.autoDiscover = false;
|
||||
let myDropzone = null;
|
||||
const dropzoneElement = document.querySelector("#dropzone-container");
|
||||
|
||||
if (dropzoneElement) {
|
||||
myDropzone = new Dropzone(dropzoneElement, {
|
||||
url: "/",
|
||||
autoProcessQueue: false,
|
||||
previewTemplate: previewTemplate,
|
||||
clickable: "#btn-tambah-foto",
|
||||
previewsContainer: "#dropzone-container",
|
||||
parallelUploads: 10,
|
||||
maxFilesize: 5,
|
||||
addRemoveLinks: false,
|
||||
acceptedFiles: "image/jpeg,image/jpg,image/png",
|
||||
|
||||
init: function () {
|
||||
let dz = this;
|
||||
|
||||
let rawPhotos = {!! json_encode($lahanSelected->foto ?? []) !!};
|
||||
let existingPhotos = [];
|
||||
|
||||
if (typeof rawPhotos === 'string') {
|
||||
try {
|
||||
existingPhotos = JSON.parse(rawPhotos);
|
||||
} catch(e) {
|
||||
existingPhotos = [];
|
||||
}
|
||||
} else if (Array.isArray(rawPhotos)) {
|
||||
existingPhotos = rawPhotos;
|
||||
}
|
||||
|
||||
if (existingPhotos && existingPhotos.length > 0) {
|
||||
existingPhotos.forEach(function (photo, index) {
|
||||
|
||||
let pathStr = typeof photo === 'object' ? (photo.path || photo[0] || '') : photo;
|
||||
|
||||
if (pathStr && pathStr !== '') {
|
||||
let mockFile = {
|
||||
name: "Foto Lahan " + (index + 1),
|
||||
size: 1024,
|
||||
id: index,
|
||||
isServerFile: true,
|
||||
accepted: true
|
||||
};
|
||||
|
||||
dz.emit("addedfile", mockFile);
|
||||
dz.emit("thumbnail", mockFile, "{{ asset('storage') }}/" + pathStr);
|
||||
dz.emit("complete", mockFile);
|
||||
|
||||
dz.files.push(mockFile);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.on("removedfile", function (file) {
|
||||
if (file.isServerFile) {
|
||||
let deleteInput = $('<input>').attr({
|
||||
type: 'hidden',
|
||||
name: 'deleted_photos[]',
|
||||
value: file.id
|
||||
});
|
||||
$('#deleted-photos-container').append(deleteInput);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on('click', '.btn-edit', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
let previewElement = $(this).closest('.dz-preview')[0];
|
||||
let dz = myDropzone;
|
||||
let fileToReplace = dz.files.find(f => f.previewElement === previewElement);
|
||||
|
||||
if (fileToReplace) {
|
||||
let tempInput = document.createElement('input');
|
||||
tempInput.type = 'file';
|
||||
tempInput.accept = 'image/jpeg,image/jpg,image/png';
|
||||
|
||||
tempInput.onchange = function(event) {
|
||||
let newFile = event.target.files[0];
|
||||
if (newFile) {
|
||||
if(fileToReplace.isServerFile){
|
||||
let deleteInput = $('<input>').attr({
|
||||
type: 'hidden',
|
||||
name: 'deleted_photos[]',
|
||||
value: fileToReplace.id
|
||||
});
|
||||
$('#deleted-photos-container').append(deleteInput);
|
||||
}
|
||||
|
||||
dz.removeFile(fileToReplace);
|
||||
dz.addFile(newFile);
|
||||
}
|
||||
};
|
||||
tempInput.click();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '#dropzone-container .dz-image img', function (e) {
|
||||
e.stopPropagation();
|
||||
$('#foto-lightbox-img').attr('src', this.src);
|
||||
$('#foto-lightbox').addClass('active');
|
||||
});
|
||||
|
||||
$('#foto-lightbox').on('click', function (e) {
|
||||
if (e.target === this || $(e.target).hasClass('foto-lightbox-close')) {
|
||||
$(this).removeClass('active');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// FORM VALIDATION & SUBMIT
|
||||
$('#petani-lahan-tanaman').validate({
|
||||
rules: {
|
||||
luas_lahan: { required: true },
|
||||
@@ -697,6 +863,57 @@
|
||||
},
|
||||
unhighlight: function (element) {
|
||||
$(element).removeClass('is-invalid');
|
||||
},
|
||||
submitHandler: function (form) {
|
||||
const formData = new FormData(form);
|
||||
|
||||
formData.append('_method', 'PUT');
|
||||
|
||||
if (myDropzone) {
|
||||
myDropzone.files.forEach(function(file) {
|
||||
if (!file.isServerFile) {
|
||||
formData.append("foto[]", file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let submitBtn = $(form).find('button[type="submit"]');
|
||||
let originalText = submitBtn.html();
|
||||
submitBtn.prop('disabled', true).html('<i class="ti tabler-loader fa-spin me-2"></i> Menyimpan...');
|
||||
|
||||
$.ajax({
|
||||
url: $(form).attr('action'),
|
||||
type: "POST",
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': $('input[name="_token"]').val(),
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
success: function(res) {
|
||||
alert('Data lahan berhasil disimpan!');
|
||||
window.location.href = "{{ route('petani.index') }}";
|
||||
},
|
||||
error: function(xhr) {
|
||||
submitBtn.prop('disabled', false).html(originalText);
|
||||
|
||||
if (xhr.status === 422) {
|
||||
let errors = xhr.responseJSON.errors;
|
||||
let errorMsg = "";
|
||||
$.each(errors, function(key, val) {
|
||||
errorMsg += val[0] + "\n";
|
||||
});
|
||||
alert("Penyimpanan Gagal:\n\n" + errorMsg);
|
||||
} else {
|
||||
let errorDetail = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : xhr.statusText;
|
||||
alert("Error " + xhr.status + ": " + errorDetail);
|
||||
console.error(xhr.responseText);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user