switch from ownCloud to minIO
buttons in table are buttons and do edit and delete properlymaster
parent
4603953458
commit
c1183c22ea
@ -0,0 +1,11 @@
|
|||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def distinct_vehicles(vehicles):
|
||||||
|
vehicles_set = set(vehicles.split(','))
|
||||||
|
vehicles_str = ', '.join(sorted(vehicles_set))
|
||||||
|
return vehicles_str
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
from minio import Minio
|
||||||
|
from django.conf import settings
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
import boto3
|
||||||
|
from botocore.client import Config
|
||||||
|
|
||||||
|
client = Minio(
|
||||||
|
settings.MINIO_ENDPOINT,
|
||||||
|
access_key=settings.MINIO_ACCESS_KEY,
|
||||||
|
secret_key=settings.MINIO_SECRET_KEY,
|
||||||
|
secure=settings.MINIO_SECURE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_damage_photo(photo_file, depot_id, content_type):
|
||||||
|
try:
|
||||||
|
s3_client = boto3.client('s3',
|
||||||
|
endpoint_url=f'http://{settings.MINIO_ENDPOINT}',
|
||||||
|
aws_access_key_id=settings.MINIO_ACCESS_KEY,
|
||||||
|
aws_secret_access_key=settings.MINIO_SECRET_KEY,
|
||||||
|
config=Config(signature_version='s3v4'),
|
||||||
|
region_name='us-east-1'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use the full filename as the key, which includes the depot_id folder
|
||||||
|
s3_client.upload_fileobj(
|
||||||
|
photo_file,
|
||||||
|
settings.MINIO_BUCKET_NAME,
|
||||||
|
photo_file.name,
|
||||||
|
ExtraArgs={'ContentType': content_type}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate a presigned URL for the uploaded object
|
||||||
|
url = s3_client.generate_presigned_url('get_object',
|
||||||
|
Params={
|
||||||
|
'Bucket': settings.MINIO_BUCKET_NAME,
|
||||||
|
'Key': photo_file.name
|
||||||
|
},
|
||||||
|
ExpiresIn=3600
|
||||||
|
)
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error uploading to MinIO: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def get_damages(depot_id):
|
||||||
|
try:
|
||||||
|
objects = client.list_objects(
|
||||||
|
settings.MINIO_BUCKET_NAME,
|
||||||
|
prefix=f"{depot_id}/"
|
||||||
|
)
|
||||||
|
|
||||||
|
damages = []
|
||||||
|
for obj in objects:
|
||||||
|
url = client.presigned_get_object(
|
||||||
|
settings.MINIO_BUCKET_NAME,
|
||||||
|
obj.object_name,
|
||||||
|
expires=timedelta(days=7)
|
||||||
|
)
|
||||||
|
damages.append({
|
||||||
|
"url": url,
|
||||||
|
"filename": obj.object_name.split('/')[-1]
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response(damages, status=status.HTTP_200_OK)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error listing objects from MinIO: {str(e)}")
|
||||||
|
return Response([], status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
@ -0,0 +1,129 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
from django.views.generic import FormView, ListView
|
||||||
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
|
from common.utils.utils import filter_queryset_by_user
|
||||||
|
from containers.forms import ContainerSearchForm
|
||||||
|
from containers.models import Container
|
||||||
|
|
||||||
|
|
||||||
|
# class ContainerDetails(FormView):
|
||||||
|
# template_name = 'common/container-details.html'
|
||||||
|
# form_class = ContainerSearchForm
|
||||||
|
# model = Container
|
||||||
|
# base_template = 'employee-base.html'
|
||||||
|
# context_object_name = 'objects'
|
||||||
|
# paginate_by = 10
|
||||||
|
#
|
||||||
|
# def get_context_data(self, **kwargs):
|
||||||
|
# context = super().get_context_data(**kwargs)
|
||||||
|
# context['base_template'] = self.base_template
|
||||||
|
# return context
|
||||||
|
#
|
||||||
|
# def get(self, request, *args, **kwargs):
|
||||||
|
#
|
||||||
|
# referrer = request.META.get('HTTP_REFERER', '')
|
||||||
|
# if 'container-details' not in referrer:
|
||||||
|
# request.session.pop('container_number', None)
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# context = self.get_context_data()
|
||||||
|
# selected_id = request.GET.get('selected')
|
||||||
|
# container_number = request.session.get('container_number')
|
||||||
|
#
|
||||||
|
# if container_number:
|
||||||
|
# containers = Container.objects.filter(number=container_number).order_by('-received_on')
|
||||||
|
# if containers.exists():
|
||||||
|
# if selected_id:
|
||||||
|
# try:
|
||||||
|
# selected_container = containers.get(id=selected_id)
|
||||||
|
# except Container.DoesNotExist:
|
||||||
|
# selected_container = containers.first()
|
||||||
|
# else:
|
||||||
|
# selected_container = containers.first()
|
||||||
|
#
|
||||||
|
# context.update({
|
||||||
|
# 'form': self.form_class(initial={'container_number': container_number}),
|
||||||
|
# 'container': selected_container,
|
||||||
|
# 'objects': containers, # For list-crud.html compatibility
|
||||||
|
# 'show_results': True
|
||||||
|
# })
|
||||||
|
# return render(request, self.template_name, context)
|
||||||
|
#
|
||||||
|
# context.update({
|
||||||
|
# 'form': self.form_class(),
|
||||||
|
# 'show_results': False
|
||||||
|
# })
|
||||||
|
# return render(request, self.template_name, context)
|
||||||
|
#
|
||||||
|
# def form_valid(self, form):
|
||||||
|
# container_number = form.cleaned_data['container_number']
|
||||||
|
# self.request.session['container_number'] = container_number # Store in session
|
||||||
|
# context = self.get_context_data()
|
||||||
|
# containers = Container.objects.filter(number=container_number).order_by('-received_on')
|
||||||
|
# if containers.exists():
|
||||||
|
# context.update({
|
||||||
|
# 'form': form,
|
||||||
|
# 'container': containers.first(),
|
||||||
|
# 'objects': containers, # For list-crud.html compatibility
|
||||||
|
# 'show_results': True
|
||||||
|
# })
|
||||||
|
# else:
|
||||||
|
# context.update({
|
||||||
|
# 'form': form,
|
||||||
|
# 'error': 'Container not found',
|
||||||
|
# 'show_results': False
|
||||||
|
# })
|
||||||
|
# return render(self.request, self.template_name, context)
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerDetails(ListView):
|
||||||
|
template_name = 'common/container-details.html'
|
||||||
|
model = Container
|
||||||
|
base_template = 'employee-base.html'
|
||||||
|
context_object_name = 'objects'
|
||||||
|
paginate_by = 10
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
container_number = self.request.session.get('container_number')
|
||||||
|
if container_number:
|
||||||
|
query = Container.objects.filter(number=container_number).order_by('-received_on')
|
||||||
|
query = filter_queryset_by_user(query, self.request.user)
|
||||||
|
return query
|
||||||
|
|
||||||
|
return Container.objects.none()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['base_template'] = self.base_template
|
||||||
|
context['form'] = ContainerSearchForm(
|
||||||
|
initial={'container_number': self.request.session.get('container_number')}
|
||||||
|
)
|
||||||
|
|
||||||
|
full_queryset = self.get_queryset()
|
||||||
|
if full_queryset.exists():
|
||||||
|
selected_id = self.request.GET.get('selected')
|
||||||
|
if selected_id:
|
||||||
|
try:
|
||||||
|
context['container'] = full_queryset.get(id=selected_id)
|
||||||
|
except Container.DoesNotExist:
|
||||||
|
context['container'] = full_queryset.first()
|
||||||
|
else:
|
||||||
|
context['container'] = full_queryset.first()
|
||||||
|
context['show_results'] = True
|
||||||
|
else:
|
||||||
|
context['show_results'] = False
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
form = ContainerSearchForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
container_number = form.cleaned_data['container_number']
|
||||||
|
request.session['container_number'] = container_number
|
||||||
|
return self.get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
context = self.get_context_data()
|
||||||
|
context['form'] = form
|
||||||
|
context['show_results'] = False
|
||||||
|
return render(request, self.template_name, context)
|
||||||
@ -1,52 +1,85 @@
|
|||||||
import base64
|
import base64
|
||||||
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import boto3
|
||||||
|
from botocore.client import Config
|
||||||
from django.core.exceptions import BadRequest
|
from django.core.exceptions import BadRequest
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from common.utils.owncloud_utls import Owncloud
|
# from common.utils.owncloud_utls import Owncloud
|
||||||
from containers.models import ContainerPhotos, Container
|
from containers.models import ContainerPhotos, Container
|
||||||
|
from common.utils.minio_utils import get_damages, upload_damage_photo
|
||||||
|
|
||||||
|
|
||||||
class Damages(APIView):
|
class Damages(APIView):
|
||||||
|
|
||||||
def post(self, request, depot_id):
|
def post(self, request, depot_id):
|
||||||
photo = request.data.get("photo")
|
photo = request.data.get("photo")
|
||||||
extension = request.data.get("photo_extension")
|
extension = request.data.get("photo_extension")
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=f'.{extension}', delete=False) as temp_file:
|
|
||||||
try:
|
try:
|
||||||
temp_file.write(base64.b64decode(photo.encode("utf-8")))
|
# Decode base64 and create a ContentFile
|
||||||
temp_file.flush()
|
photo_data = base64.b64decode(photo.encode("utf-8"))
|
||||||
temp_file.close() # Close the file before uploading
|
|
||||||
|
# Generate a unique filename with proper folder structure
|
||||||
own_filename = Owncloud.upload_damage_photo(temp_file.name, depot_id)
|
import uuid
|
||||||
|
filename = f"{depot_id}/{datetime.today().strftime('%Y%m%d')}_{uuid.uuid4()}.{extension}"
|
||||||
container_photo = ContainerPhotos()
|
photo_file = ContentFile(photo_data, name=filename)
|
||||||
container_photo.container = Container.objects.get(pk=depot_id)
|
|
||||||
container_photo.photo = own_filename
|
# Set content type based on file extension
|
||||||
container_photo.uploaded_on = datetime.now()
|
content_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
||||||
container_photo.uploaded_by = request.user
|
|
||||||
container_photo.save()
|
# Upload to MinIO and get URL
|
||||||
|
url = upload_damage_photo(photo_file, depot_id, content_type)
|
||||||
|
|
||||||
|
# Save to database
|
||||||
|
container_photo = ContainerPhotos.objects.create(
|
||||||
|
container=Container.objects.get(pk=depot_id),
|
||||||
|
photo=url,
|
||||||
|
uploaded_by=request.user
|
||||||
|
)
|
||||||
|
|
||||||
return Response(status=status.HTTP_201_CREATED)
|
return Response(status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
print(f"Error in damage photo upload: {str(ex)}")
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Failed to process photo"},
|
{"error": "Failed to process photo"},
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
finally:
|
|
||||||
if temp_file:
|
|
||||||
try:
|
|
||||||
os.unlink(temp_file.name)
|
|
||||||
except OSError:
|
|
||||||
pass # Ignore deletion errors
|
|
||||||
def get(self, request, depot_id):
|
def get(self, request, depot_id):
|
||||||
return Owncloud.get_damages(depot_id)
|
try:
|
||||||
|
s3_client = boto3.client('s3',
|
||||||
|
endpoint_url=f"http://{settings.MINIO_ENDPOINT}",
|
||||||
|
aws_access_key_id=settings.MINIO_ACCESS_KEY,
|
||||||
|
aws_secret_access_key=settings.MINIO_SECRET_KEY,
|
||||||
|
config=Config(signature_version='s3v4')
|
||||||
|
)
|
||||||
|
|
||||||
|
prefix = f"{depot_id}/"
|
||||||
|
response = s3_client.list_objects_v2(
|
||||||
|
Bucket=settings.MINIO_BUCKET_NAME,
|
||||||
|
Prefix=prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
photos = []
|
||||||
|
if 'Contents' in response:
|
||||||
|
for obj in response['Contents']:
|
||||||
|
url = f"{settings.MINIO_SERVER_URL}/{settings.MINIO_BUCKET_NAME}/{obj['Key']}"
|
||||||
|
photos.append({'url': url})
|
||||||
|
|
||||||
|
return Response(photos, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"MinIO Error: {str(e)}")
|
||||||
|
return Response(
|
||||||
|
{"error": "Failed to retrieve photos"},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: postgres
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=depot # Matches DB_NAME in production.env
|
||||||
|
- POSTGRES_USER=postgres # Matches DB_USER in production.env
|
||||||
|
- POSTGRES_PASSWORD=admin # Matches DB_PASSWORD in production.env
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d depot"] # Check specific database
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
ports:
|
||||||
|
- "9000:9000" # API Port
|
||||||
|
- "9001:9001" # Console Port
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: kikimor # Change this
|
||||||
|
MINIO_ROOT_PASSWORD: shushunka1
|
||||||
|
MINIO_SERVER_URL: http://localhost:9000
|
||||||
|
MINIO_ADDRESS: "0.0.0.0:9000"
|
||||||
|
MINIO_BROWSER_REDIRECT_URL: "http://localhost:9001" # Console URL
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
command: server --console-address ":9001" /data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mc", "ready", "local"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
# Optional: Create buckets and users on startup
|
||||||
|
createbuckets:
|
||||||
|
image: minio/mc
|
||||||
|
depends_on:
|
||||||
|
- minio
|
||||||
|
entrypoint: >
|
||||||
|
/bin/sh -c "
|
||||||
|
sleep 10;
|
||||||
|
mc alias set myminio http://minio:9000 kikimor shushunka1;
|
||||||
|
mc mb myminio/damages;
|
||||||
|
mc anonymous set download myminio/damages;
|
||||||
|
mc policy set public myminio/damages;
|
||||||
|
echo '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:GetObject\"],\"Resource\":[\"arn:aws:s3:::damages/*\"]}]}' > /tmp/policy.json;
|
||||||
|
mc admin policy add myminio getonly /tmp/policy.json;
|
||||||
|
mc admin policy set myminio getonly user=kikimor;
|
||||||
|
exit 0;
|
||||||
|
"
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
minio_data:
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
@ -1,11 +1,84 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
database:
|
||||||
|
image: postgres
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=depot # Matches DB_NAME in production.env
|
||||||
|
- POSTGRES_USER=postgres # Matches DB_USER in production.env
|
||||||
|
- POSTGRES_PASSWORD=admin # Matches DB_PASSWORD in production.env
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d depot"] # Check specific database
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
env_file:
|
env_file:
|
||||||
- development.env
|
- production.env
|
||||||
|
depends_on:
|
||||||
|
database:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
command: >
|
||||||
|
bash -c "python manage.py migrate &&
|
||||||
|
python manage.py runserver 0.0.0.0:8000"
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
ports:
|
||||||
|
- "9000:9000" # API Port
|
||||||
|
- "9001:9001" # Console Port
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: kikimor # Change this
|
||||||
|
MINIO_ROOT_PASSWORD: shushunka1
|
||||||
|
MINIO_SERVER_URL: http://localhost:9000
|
||||||
|
MINIO_ADDRESS: "0.0.0.0:9000"
|
||||||
|
MINIO_BROWSER_REDIRECT_URL: "http://localhost:9001" # Console URL
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
command: server --console-address ":9001" /data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mc", "ready", "local"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
# Optional: Create buckets and users on startup
|
||||||
|
createbuckets:
|
||||||
|
image: minio/mc
|
||||||
|
depends_on:
|
||||||
|
- minio
|
||||||
|
entrypoint: >
|
||||||
|
/bin/sh -c "
|
||||||
|
sleep 10;
|
||||||
|
mc alias set myminio http://minio:9000 kikimor shushunka1;
|
||||||
|
mc mb myminio/damages;
|
||||||
|
mc anonymous set download myminio/damages;
|
||||||
|
mc policy set public myminio/damages;
|
||||||
|
echo '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:GetObject\"],\"Resource\":[\"arn:aws:s3:::damages/*\"]}]}' > /tmp/policy.json;
|
||||||
|
mc admin policy add myminio getonly /tmp/policy.json;
|
||||||
|
mc admin policy set myminio getonly user=kikimor;
|
||||||
|
exit 0;
|
||||||
|
"
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- .:/DepoT
|
postgres_data:
|
||||||
|
minio_data:
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
Binary file not shown.
@ -0,0 +1,23 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const table = document.getElementById('objectTable');
|
||||||
|
if (!table) return;
|
||||||
|
|
||||||
|
table.addEventListener('click', function(e) {
|
||||||
|
const row = e.target.closest('.selectable-row');
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
// Remove selection from all rows
|
||||||
|
table.querySelectorAll('.selected-row').forEach(selectedRow => {
|
||||||
|
selectedRow.classList.remove('selected-row');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add selection to clicked row
|
||||||
|
row.classList.add('selected-row');
|
||||||
|
|
||||||
|
// Get container ID and redirect to show its details
|
||||||
|
const containerId = row.dataset.id;
|
||||||
|
const currentUrl = new URL(window.location.href);
|
||||||
|
currentUrl.searchParams.set('selected', containerId);
|
||||||
|
window.location.href = currentUrl.toString();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
.search-card {
|
||||||
|
background: #EDDECB;
|
||||||
|
border: 1px solid #E1C6A8;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-body {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*.btn-primary {*/
|
||||||
|
/* background: #EDDECB;*/
|
||||||
|
/* color: white;*/
|
||||||
|
/* border: none;*/
|
||||||
|
/* padding: 0.5rem 1rem;*/
|
||||||
|
/* border-radius: 8px;*/
|
||||||
|
/* cursor: pointer;*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
|
/*.btn-primary:hover {*/
|
||||||
|
/* background: #0056b3;*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
|
.details-card {
|
||||||
|
background: #EDDECB;
|
||||||
|
border: 1px solid #E1C6A8;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: #E1C6A8;
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid #E1C6A8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-body {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
flex: 0 0 45%;
|
||||||
|
text-align: right;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form styling */
|
||||||
|
input[type="text"] {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
@ -1,46 +1,5 @@
|
|||||||
{% load static %}
|
{% extends "common/base.html" %}
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Line Operator Dashboard | Container Depot</title>
|
|
||||||
<link rel="stylesheet" href="{% static 'styles/tables.css' %}">
|
|
||||||
<link rel="stylesheet" href="{% static 'styles/forms.css' %}">
|
|
||||||
<link rel="stylesheet" href="{% static 'styles/sidebar.css' %}">
|
|
||||||
<link rel="stylesheet" href="{% static 'styles/base.css' %}">
|
|
||||||
<link rel="stylesheet" href="{% static 'styles/dashboard-content.css' %}">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
{% block aside %}
|
||||||
<body>
|
|
||||||
<aside class="sidebar">
|
|
||||||
{% include 'client-sidebar.html' %}
|
{% include 'client-sidebar.html' %}
|
||||||
</aside>
|
{% endblock aside %}
|
||||||
|
|
||||||
<main class="content-area">
|
|
||||||
<div class="content">
|
|
||||||
{% block content %}
|
|
||||||
{% endblock content %}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{% block custom_styles %}
|
|
||||||
{% endblock custom_styles %}
|
|
||||||
|
|
||||||
{% block extra_js %}
|
|
||||||
<script>
|
|
||||||
const validateContainerUrl = "{% url 'validate_container' 'placeholder' %}".replace('placeholder/', '');
|
|
||||||
</script>
|
|
||||||
<script src="{% static 'js/container_validation.js' %}"></script>
|
|
||||||
|
|
||||||
{% endblock extra_js %}
|
|
||||||
|
|
||||||
{% block crud_js %}
|
|
||||||
{% endblock crud_js %}
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Line Operator Dashboard | Container Depot</title>
|
||||||
|
<link rel="stylesheet" href="{% static 'styles/tables.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'styles/forms.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'styles/sidebar.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'styles/base.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'styles/dashboard-content.css' %}">
|
||||||
|
{% block extra_styles %}
|
||||||
|
{% endblock extra_styles %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<aside class="sidebar">
|
||||||
|
{% block aside %}
|
||||||
|
{% endblock aside %}
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="content-area">
|
||||||
|
<div class="content">
|
||||||
|
{% block content_header %}
|
||||||
|
{% endblock content_header%}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block content_footer %}
|
||||||
|
{% endblock content_footer%}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% block custom_styles %}
|
||||||
|
{% endblock custom_styles %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
const validateContainerUrl = "{% url 'validate_container' 'placeholder' %}".replace('placeholder/', '');
|
||||||
|
</script>
|
||||||
|
<script src="{% static 'js/container_validation.js' %}"></script>
|
||||||
|
{% endblock extra_js %}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,303 @@
|
|||||||
|
{% extends 'list-crud.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block extra_styles %}
|
||||||
|
<link rel="stylesheet" href="{% static 'styles/details.css' %}">
|
||||||
|
<style>
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
padding: 20px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 1200px;
|
||||||
|
height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-modal {
|
||||||
|
color: #fff;
|
||||||
|
position: absolute;
|
||||||
|
right: 25px;
|
||||||
|
top: 10px;
|
||||||
|
font-size: 35px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.gallery-item iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.photo-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-link {
|
||||||
|
background: #E1C6A8;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-link:hover {
|
||||||
|
background: #d4b08c;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content_header %}
|
||||||
|
<div class="search-card">
|
||||||
|
<div class="search-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="search-group">
|
||||||
|
{{ form.container_number }}
|
||||||
|
<div class="search-button">
|
||||||
|
<button type="submit" class="btn-primary">Search</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if show_results and container %}
|
||||||
|
<div class="details-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Container Details</h5>
|
||||||
|
{% if container.photos.exists %}
|
||||||
|
<button type="button" class="btn-primary" onclick="openGallery()">
|
||||||
|
<i class="fas fa-images"></i> View Photos
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="details-body">
|
||||||
|
<div class="details-grid">
|
||||||
|
<div class="details-column">
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">Container Type:</div>
|
||||||
|
<div class="detail-value">{{ container.container_type }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">Line:</div>
|
||||||
|
<div class="detail-value">{{ container.line }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">Preinfo:</div>
|
||||||
|
<div class="detail-value">{{ container.preinfo }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">Received On:</div>
|
||||||
|
<div class="detail-value">{{ container.received_on|date:"Y-m-d H:i" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">Receive Vehicle:</div>
|
||||||
|
<div class="detail-value">{{ container.receive_vehicle }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="details-column">
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">Swept:</div>
|
||||||
|
<div class="detail-value">{{ container.swept|yesno:"Yes,No" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">Washed:</div>
|
||||||
|
<div class="detail-value">{{ container.washed|yesno:"Yes,No" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">Booking:</div>
|
||||||
|
<div class="detail-value">
|
||||||
|
{% if container.booking %}
|
||||||
|
{{ container.booking.name }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">Expedited On:</div>
|
||||||
|
<div class="detail-value">{{ container.expedited_on|date:"Y-m-d H:i"|default:"-" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">Expedition Vehicle:</div>
|
||||||
|
<div class="detail-value">{{ container.expedition_vehicle|default:"-" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">Damages:</div>
|
||||||
|
<div class="detail-value">{{ container.damages|default:"-" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">Heavy Damaged:</div>
|
||||||
|
<div class="detail-value">{{ container.heavy_damaged|yesno:"Yes,No" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="galleryModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close-modal" onclick="closeGallery()">×</span>
|
||||||
|
<h3 style="color: white; margin-bottom: 1rem;">Photos for Container {{ container.number }}</h3>
|
||||||
|
<div class="gallery-grid">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% endblock content_header %}
|
||||||
|
|
||||||
|
{% block table_header %}
|
||||||
|
<th style="display: none;">Select</th>
|
||||||
|
<th>Company name</th>
|
||||||
|
<th>Company short name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
{% endblock table_header %}
|
||||||
|
|
||||||
|
{% block table_data %}
|
||||||
|
<td>{{ object.number }}</td>
|
||||||
|
<td>{{ object.number }}</td>
|
||||||
|
<td>{{ object.number }}</td>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block buttons %}
|
||||||
|
{% endblock buttons %}
|
||||||
|
|
||||||
|
{% block crud_js %}
|
||||||
|
<script src="{% static 'js/container-details.js' %}"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function loadPhotos(containerId) {
|
||||||
|
const photoGrid = document.querySelector('.gallery-grid');
|
||||||
|
photoGrid.innerHTML = '<div class="loading">Loading photos...</div>';
|
||||||
|
|
||||||
|
fetch(`/api/damages/${containerId}/`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
photoGrid.innerHTML = '';
|
||||||
|
if (data.length === 0) {
|
||||||
|
photoGrid.innerHTML = '<div style="color: white;">No photos available</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.forEach(photo => {
|
||||||
|
const photoDiv = document.createElement('div');
|
||||||
|
photoDiv.className = 'gallery-item';
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = photo.url; // Use the presigned URL directly
|
||||||
|
img.alt = `Container photo`;
|
||||||
|
|
||||||
|
img.onerror = function() {
|
||||||
|
console.error('Failed to load image:', img.src);
|
||||||
|
this.style.display = 'none';
|
||||||
|
photoDiv.innerHTML += '<div style="color: red;">Failed to load image</div>';
|
||||||
|
};
|
||||||
|
|
||||||
|
photoDiv.appendChild(img);
|
||||||
|
photoGrid.appendChild(photoDiv);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading photos:', error);
|
||||||
|
photoGrid.innerHTML = '<div style="color: white;">Error loading photos</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openGallery() {
|
||||||
|
const container = {{ container.id|default:'null' }};
|
||||||
|
if (container) {
|
||||||
|
loadPhotos(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('galleryModal').style.display = 'block';
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeGallery() {
|
||||||
|
document.getElementById('galleryModal').style.display = 'none';
|
||||||
|
document.body.style.overflow = 'auto';
|
||||||
|
document.querySelector('.gallery-grid').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
window.onclick = function(event) {
|
||||||
|
const modal = document.getElementById('galleryModal');
|
||||||
|
if (event.target === modal) {
|
||||||
|
closeGallery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal on escape key
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
closeGallery();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add loading indicator styles
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.loading {
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@ -1,38 +1,5 @@
|
|||||||
{% load static %}
|
{% extends "common/base.html" %}
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Depot Employee Dashboard | Container Depot</title>
|
|
||||||
<link rel="stylesheet" href="{% static 'styles/tables.css' %}">
|
|
||||||
<link rel="stylesheet" href="{% static 'styles/forms.css' %}">
|
|
||||||
<link rel="stylesheet" href="{% static 'styles/sidebar.css' %}">
|
|
||||||
<link rel="stylesheet" href="{% static 'styles/base.css' %}">
|
|
||||||
<link rel="stylesheet" href="{% static 'styles/dashboard-content.css' %}">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
{% block aside %}
|
||||||
<body>
|
|
||||||
<aside class="sidebar">
|
|
||||||
{% include 'employee-sidebar.html' %}
|
{% include 'employee-sidebar.html' %}
|
||||||
</aside>
|
{% endblock aside %}
|
||||||
|
|
||||||
<main class="content-area">
|
|
||||||
<div class="content">
|
|
||||||
{% block content %}
|
|
||||||
{% endblock content %}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% block extra_js %}
|
|
||||||
<script>
|
|
||||||
const validateContainerUrl = "{% url 'validate_container' 'placeholder' %}".replace('placeholder/', '');
|
|
||||||
</script>
|
|
||||||
<script src="{% static 'js/container_validation.js' %}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|||||||
Loading…
Reference in New Issue