-old"> 182
+                ('MC11', models.CharField(max_length=255, null=True)),
183
+                ('MC12', models.CharField(max_length=255, null=True)),
184
+                ('MC14', models.CharField(max_length=255, null=True)),
185
+                ('MC15', models.CharField(max_length=255, null=True)),
186
+                ('MC16', models.CharField(max_length=255, null=True)),
187
+                ('MC19', models.CharField(max_length=255, null=True)),
188
+                ('MC20', models.CharField(max_length=255, null=True)),
189
+                ('MC21', models.CharField(max_length=255, null=True)),
190
+                ('MC22', models.CharField(max_length=255, null=True)),
191
+                ('MC23', models.CharField(max_length=255, null=True)),
192
+                ('MC24', models.CharField(max_length=255, null=True)),
193
+                ('MP45', models.CharField(max_length=255, null=True)),
194
+                ('MP49', models.CharField(max_length=255, null=True)),
195
+                ('MI13', models.CharField(max_length=255, null=True)),
196
+                ('TC', models.CharField(max_length=255, null=True)),
197
+                ('MI14', models.CharField(max_length=255, null=True)),
198
+                ('MI15', models.CharField(max_length=255, null=True)),
199
+                ('MI16', models.CharField(max_length=255, null=True)),
200
+                ('MI17', models.CharField(max_length=255, null=True)),
201
+                ('MI18', models.CharField(max_length=255, null=True)),
202
+                ('MI19', models.CharField(max_length=255, null=True)),
203
+                ('MI20', models.CharField(max_length=255, null=True)),
204
+                ('MI21', models.CharField(max_length=255, null=True)),
205
+                ('MI22', models.CharField(max_length=255, null=True)),
206
+                ('MI23', models.CharField(max_length=255, null=True)),
207
+                ('MI31', models.CharField(max_length=255, null=True)),
208
+                ('MI33', models.CharField(max_length=255, null=True)),
209
+                ('INSAGM', models.CharField(max_length=255, null=True)),
210
+                ('MARAGM', models.CharField(max_length=255, null=True)),
211
+                ('MI53', models.CharField(max_length=255, null=True)),
212
+                ('MI55', models.CharField(max_length=255, null=True)),
213
+                ('MI36', models.CharField(max_length=255, null=True)),
214
+                ('MI39', models.CharField(max_length=255, null=True)),
215
+                ('MI24', models.CharField(max_length=255, null=True)),
216
+                ('Ind1', models.CharField(max_length=255, null=True)),
217
+                ('Ind2', models.CharField(max_length=255, null=True)),
218
+                ('Ind3', models.CharField(max_length=255, null=True)),
219
+                ('AGR1', models.CharField(max_length=255, null=True)),
220
+                ('AGR2', models.CharField(max_length=255, null=True)),
221
+                ('ob_Condition', models.CharField(max_length=255, null=True)),
222
+            ],
223
+            options={
224
+                'db_table': 'mg_master_view',
225
+                'managed': False,
226
+            },
227
+        ),
228
+        migrations.CreateModel(
229
+            name='VMasterView',
230
+            fields=[
231
+                ('PRO0', models.CharField(max_length=255, primary_key=True, serialize=False)),
232
+                ('PRO1', models.CharField(max_length=255, null=True)),
233
+                ('PRO1C', models.CharField(max_length=255, null=True)),
234
+                ('PRO2', models.CharField(max_length=255, null=True)),
235
+                ('PRO5', models.CharField(max_length=255, null=True)),
236
+                ('PRO8', models.CharField(max_length=255, null=True)),
237
+                ('PRO9', models.CharField(max_length=255, null=True)),
238
+                ('PRO10', models.CharField(max_length=255, null=True)),
239
+                ('PRO11', models.CharField(max_length=255, null=True)),
240
+                ('PRO12', models.CharField(max_length=255, null=True)),
241
+                ('PRO13', models.CharField(max_length=255, null=True)),
242
+                ('PRO14', models.CharField(max_length=255, null=True)),
243
+                ('PRO15', models.CharField(max_length=255, null=True)),
244
+                ('PRO16', models.CharField(max_length=255, null=True)),
245
+                ('PRO17', models.CharField(max_length=255, null=True)),
246
+                ('PRO18', models.CharField(max_length=255, null=True)),
247
+                ('PRO21', models.CharField(max_length=255, null=True)),
248
+                ('PRO25', models.CharField(max_length=255, null=True)),
249
+                ('PRO27', models.CharField(max_length=255, null=True)),
250
+                ('P2', models.CharField(max_length=255, null=True)),
251
+                ('PRO6', models.CharField(max_length=255, null=True)),
252
+                ('SPEED', models.CharField(max_length=255, null=True)),
253
+                ('PRO4', models.CharField(max_length=255, null=True)),
254
+                ('MC11', models.CharField(max_length=255, null=True)),
255
+                ('MC12', models.CharField(max_length=255, null=True)),
256
+                ('MC14', models.CharField(max_length=255, null=True)),
257
+                ('MC15', models.CharField(max_length=255, null=True)),
258
+                ('MC16', models.CharField(max_length=255, null=True)),
259
+                ('MC19', models.CharField(max_length=255, null=True)),
260
+                ('MC20', models.CharField(max_length=255, null=True)),
261
+                ('MC21', models.CharField(max_length=255, null=True)),
262
+                ('MC22', models.CharField(max_length=255, null=True)),
263
+                ('MC23', models.CharField(max_length=255, null=True)),
264
+                ('MC24', models.CharField(max_length=255, null=True)),
265
+                ('MP34', models.CharField(max_length=255, null=True)),
266
+                ('MP39', models.CharField(max_length=255, null=True)),
267
+                ('MI13', models.CharField(max_length=255, null=True)),
268
+                ('TC', models.CharField(max_length=255, null=True)),
269
+                ('MI14', models.CharField(max_length=255, null=True)),
270
+                ('MI15', models.CharField(max_length=255, null=True)),
271
+                ('MI16', models.CharField(max_length=255, null=True)),
272
+                ('MI17', models.CharField(max_length=255, null=True)),
273
+                ('MI18', models.CharField(max_length=255, null=True)),
274
+                ('MI19', models.CharField(max_length=255, null=True)),
275
+                ('MI20', models.CharField(max_length=255, null=True)),
276
+                ('MI21', models.CharField(max_length=255, null=True)),
277
+                ('MI22', models.CharField(max_length=255, null=True)),
278
+                ('MI23', models.CharField(max_length=255, null=True)),
279
+                ('MI31', models.CharField(max_length=255, null=True)),
280
+                ('MI33', models.CharField(max_length=255, null=True)),
281
+                ('INSAGM', models.CharField(max_length=255, null=True)),
282
+                ('MARAGM', models.CharField(max_length=255, null=True)),
283
+                ('MI53', models.CharField(max_length=255, null=True)),
284
+                ('MI55', models.CharField(max_length=255, null=True)),
285
+                ('MI36', models.CharField(max_length=255, null=True)),
286
+                ('MI39', models.CharField(max_length=255, null=True)),
287
+                ('MI24', models.CharField(max_length=255, null=True)),
288
+                ('Ind1', models.CharField(max_length=255, null=True)),
289
+                ('Ind2', models.CharField(max_length=255, null=True)),
290
+                ('Ind3', models.CharField(max_length=255, null=True)),
291
+                ('AGR1', models.CharField(max_length=255, null=True)),
292
+                ('AGR2', models.CharField(max_length=255, null=True)),
293
+                ('ob_Condition', models.CharField(max_length=255, null=True)),
294
+            ],
295
+            options={
296
+                'db_table': 'v_master_view',
297
+                'managed': False,
298
+            },
299
+        ),
300
+    ]

BIN
app/report/checkbox_checked.jpg


BIN
app/report/checkbox_unchecked.jpg


BIN
app/report/coi_templates.xlsx


+ 168 - 0
app/report/gen_report.py

@@ -0,0 +1,168 @@
1
+
2
+from openpyxl import load_workbook, Workbook
3
+from datetime import datetime
4
+from openpyxl.drawing.image import Image
5
+import re
6
+
7
+from openpyxl.drawing.spreadsheet_drawing import AbsoluteAnchor
8
+from openpyxl.drawing.xdr import XDRPoint2D, XDRPositiveSize2D
9
+from openpyxl.utils.units import pixels_to_EMU
10
+from openpyxl.utils import get_column_letter
11
+
12
+def center_image_in_cell(ws, cell_address, img_path):
13
+    """
14
+    Center an image in a specific cell.
15
+
16
+    Args:
17
+        ws: The worksheet object.
18
+        cell_address (str): The cell address (e.g., "B2") where the image will be centered.
19
+        img_path (str): Path to the image file.
20
+
21
+    Returns:
22
+        None
23
+    """
24
+    # Load the image
25
+    img = Image(img_path)
26
+    img.width = img.height = 20
27
+
28
+    # Get the cell
29
+    cell = ws[cell_address]
30
+
31
+    # Approximate pixel dimensions of the cell
32
+    col_letter = get_column_letter(cell.column)
33
+    col_width = ws.column_dimensions[col_letter].width or 10  # Default width
34
+    row_height = ws.row_dimensions[cell.row].height or 15  # Default height
35
+
36
+    # Convert dimensions to pixels
37
+    col_width_pixels = col_width * 7  # Approximation: 1 Excel unit = ~7 pixels
38
+    row_height_pixels = row_height * 0.75  # Approximation: 1 Excel unit = ~0.75 pixels
39
+
40
+    # Calculate the center position
41
+    cell_left = pixels_to_EMU(cell.column - 1)  # Column start position in EMU
42
+    cell_top = pixels_to_EMU((cell.row - 1) * row_height_pixels)  # Row start position in EMU
43
+    x_center = cell_left + pixels_to_EMU((col_width_pixels - img.width) / 2)
44
+    y_center = cell_top + pixels_to_EMU((row_height_pixels - img.height) / 2)
45
+
46
+    # Set the image position and size
47
+    position = XDRPoint2D(x_center, y_center)
48
+    size = XDRPositiveSize2D(pixels_to_EMU(img.width), pixels_to_EMU(img.height))
49
+    img.anchor = AbsoluteAnchor(pos=position, ext=size)
50
+
51
+    # Add the image to the worksheet
52
+    ws.add_image(img)
53
+
54
+
55
+def gen_xlsx(template_file, selected_sheets, prefix_filename, data):
56
+    """
57
+    Generate an Excel file from a template, fill placeholders, and include only selected sheets.
58
+
59
+    Args:
60
+        template_file (str): Path to the Excel template file.
61
+        selected_sheets (list): List of sheet names to include in the output file.
62
+        prefix_filename (str): Prefix for the output filename.
63
+        data (dict): Data dictionary with sheet-specific keys and fallback keys.
64
+
65
+    Returns:
66
+        str: Path of the generated Excel file.
67
+    """
68
+    checked_image_path = "/app/report/checkbox_checked.jpg"  # Path to the checked checkbox image
69
+    unchecked_image_path = "/app/report/checkbox_unchecked.jpg"  # Path to the unchecked checkbox image
70
+
71
+    # Load the template workbook
72
+    workbook = load_workbook(template_file)
73
+
74
+    # Remove sheets not in selected_sheets
75
+    for sheet_name in workbook.sheetnames:
76
+        if sheet_name not in selected_sheets:
77
+            del workbook[sheet_name]
78
+
79
+    # Process the selected sheets
80
+    for sheet_name in selected_sheets:
81
+        if sheet_name not in workbook.sheetnames:
82
+            raise ValueError(f"Sheet '{sheet_name}' not found in the template.")
83
+
84
+        sheet = workbook[sheet_name]
85
+
86
+        # Replace placeholders with actual values
87
+        # Handle hiding rows based on patterns in data
88
+        
89
+        for row in sheet.iter_rows():
90
+            for cell in row:
91
+                if cell.value and isinstance(cell.value, str) and cell.value.startswith("<") and cell.value.endswith(">"):
92
+                    placeholder = cell.value.strip("<>")
93
+                    
94
+                    # Determine value priority: `sheet_name.key` > `key`
95
+                    value = None
96
+                    sheet_specific_key = f"{sheet_name}.{placeholder}"
97
+                    if sheet_specific_key in data:
98
+                        value = data[sheet_specific_key]
99
+                    elif placeholder in data:
100
+                        value = data[placeholder]
101
+
102
+                    if value is not None:
103
+                        if value is True:
104
+                            img = Image(checked_image_path)
105
+                            img.width = img.height = 20
106
+                            print(f"{cell.coordinate}")
107
+                            sheet.add_image(img, cell.coordinate)
108
+                            cell.value = None  # Remove the placeholder text
109
+                        elif value is False:
110
+                            img = Image(unchecked_image_path)
111
+                            img.width = img.height = 20
112
+                            sheet.add_image(img, cell.coordinate)
113
+                            # center_image_in_cell(sheet, cell.coordinate, unchecked_image_path)
114
+                            cell.value = None  # Remove the placeholder text
115
+                        else:
116
+                            # Insert the text value directly
117
+                            cell.value = value
118
+
119
+        for key, value in data.items():
120
+            if isinstance(value, str) and re.match(r"^\d+\[\d+:\d+\]$", value):
121
+                # Parse the prefix and row range
122
+                prefix, row_range = value.split("[")
123
+                row_start, row_end = map(int, row_range[:-1].split(":"))
124
+                
125
+                # Hide rows if the prefix matches the condition
126
+                if prefix == "0":  # Adjust the condition as needed
127
+                    sheet.row_dimensions.group(row_start, row_end, hidden=True)
128
+        
129
+
130
+    # Generate the output filename with a timestamp
131
+    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
132
+    output_path = f"{prefix_filename}_{timestamp}.xlsx"
133
+    workbook.save(output_path)
134
+
135
+    return output_path
136
+
137
+if __name__ == "__main__":
138
+    # Example usage
139
+    data = {
140
+        "customer": "Tum Coder",
141
+        "inspect_date": "2025-01-15",
142
+        "lot_no": "12345",
143
+        "staff_name":  "Tum 8888",
144
+        "man_name":  "Tum 999",
145
+        "size": "Large",
146
+        "pcs": "10 pcs",
147
+        "spec": "Spec-A",
148
+        "hardness.d1_act": "10",
149
+        "hardness.d2_act": "0[24:28]",  # Hide rows 24 to 28 if the prefix is "0"
150
+        "hardness.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
151
+        "hardness.spe_acc": False,  # Hide rows 24 to 28 if the prefix is "0"
152
+        "dimension_app.d1_act": "33",
153
+        "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
154
+        "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
155
+        "dimension_app.spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
156
+    }
157
+
158
+
159
+
160
+
161
+    output_file = gen_xlsx(
162
+        template_file="./hardness.xlsx",
163
+        selected_sheets=["hardness", "dimension_app"],  # Replace with your actual sheet names
164
+        prefix_filename="./output/output",
165
+        data=data
166
+    )
167
+
168
+    print(f"Generated file: {output_file}")

+ 42 - 0
app/report/views.py

@@ -8,6 +8,10 @@ from .filters import ReportFilter
8 8
 from .forms import ExportOptionsForm
9 9
 from pprint import pprint
10 10
 
11
+from .gen_report import gen_xlsx
12
+from django.core.files.base import File
13
+from pathlib import Path
14
+
11 15
 
12 16
 def index(request):
13 17
     reports = Report.objects.all()
@@ -61,6 +65,44 @@ class ReportCRUDView(ConfigurableCRUDView):
61 65
 def coi_view(request):
62 66
     pprint(f"xxxx method = xxx {request.method}")
63 67
     if request.method == "POST":
68
+        pprint(request.POST)
69
+        if 'export' in request.POST:
70
+
71
+            data = {
72
+                "customer": "Tum Coder",
73
+                "inspect_date": "2025-01-15",
74
+                "lot_no": "12345",
75
+                "staff_name":  "Tum 8888",
76
+                "man_name":  "Tum 999",
77
+                "size": "Large",
78
+                "pcs": "10 pcs",
79
+                "spec": "Spec-A",
80
+                "hardness.d1_act": "10",
81
+                "hardness.d2_act": "0[24:28]",  # Hide rows 24 to 28 if the prefix is "0"
82
+                "hardness.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
83
+                "hardness.spe_acc": False,  # Hide rows 24 to 28 if the prefix is "0"
84
+                "dimension_app.d1_act": "33",
85
+                "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
86
+                "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
87
+                "dimension_app.spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
88
+            }
89
+            output_file = gen_xlsx(
90
+                template_file="/app/report/coi_templates.xlsx",
91
+                selected_sheets=["hardness", "dimension_app"],  # Replace with your actual sheet names
92
+                prefix_filename="/app/media/coi",
93
+                data=data
94
+            )
95
+            report = Report.objects.create(
96
+                name=request.POST.get('lot_no','Untitled'),
97
+                created_by=request.user,
98
+                file=None  # Leave this as None or assign a file if required
99
+            )
100
+            output_file_path = Path(output_file)  # Convert to a Path object for convenience
101
+            with open(output_file_path, "rb") as f:
102
+                report.file.save(output_file_path.name, File(f), save=True)
103
+
104
+            pprint(f"outputfile = {output_file}")
105
+
64 106
         if 'search_lot' in request.POST:
65 107
             lot_no = request.POST.get('lot_no', None)
66 108
             if lot_no:

+ 3 - 0
app/sysadmin/apps.py

@@ -4,3 +4,6 @@ from django.apps import AppConfig
4 4
 class SysadminConfig(AppConfig):
5 5
     default_auto_field = 'django.db.models.BigAutoField'
6 6
     name = 'sysadmin'
7
+
8
+    def ready(self):
9
+        import sysadmin.signals

+ 8 - 0
app/sysadmin/forms.py

@@ -14,6 +14,8 @@ class CustomLoginForm(AuthenticationForm):
14 14
 from django.contrib.auth.forms import UserCreationForm
15 15
 from django.contrib.auth.models import User
16 16
 
17
+from .models import UserProfile
18
+
17 19
 class CustomUserCreationForm(UserCreationForm):
18 20
     class Meta:
19 21
         model = User
@@ -33,3 +35,9 @@ class CustomUserCreationForm(UserCreationForm):
33 35
         self.fields['password2'].widget.attrs.update({
34 36
             'placeholder': 'Confirm Password'
35 37
         })
38
+
39
+
40
+class UserProfileForm(forms.ModelForm):
41
+    class Meta:
42
+        model = UserProfile
43
+        fields = ['profile_picture', 'position', 'signed_picture']  # Include the fields you want to manage

+ 29 - 0
app/sysadmin/migrations/0001_initial.py

@@ -0,0 +1,29 @@
1
+# Generated by Django 4.2 on 2025-01-17 04:58
2
+
3
+from django.conf import settings
4
+from django.db import migrations, models
5
+import django.db.models.deletion
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    initial = True
11
+
12
+    dependencies = [
13
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+    ]
15
+
16
+    operations = [
17
+        migrations.CreateModel(
18
+            name='UserProfile',
19
+            fields=[
20
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
+                ('bio', models.TextField(blank=True, null=True)),
22
+                ('profile_picture', models.ImageField(blank=True, null=True, upload_to='profile/%Y/%m/%d/')),
23
+                ('signed_picture', models.ImageField(blank=True, null=True, upload_to='signed/%Y/%m/%d/')),
24
+                ('email', models.EmailField(blank=True, max_length=254, null=True)),
25
+                ('position', models.CharField(blank=True, choices=[('QA_STAFF', 'QA Staff'), ('QA_MANAGER', 'QA Manager')], max_length=20, null=True)),
26
+                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
27
+            ],
28
+        ),
29
+    ]

+ 17 - 0
app/sysadmin/models.py

@@ -1,3 +1,20 @@
1 1
 from django.db import models
2 2
 
3 3
 # Create your models here.
4
+
5
+from django.contrib.auth.models import User
6
+
7
+class UserProfile(models.Model):
8
+    POSITION_CHOICES = [
9
+        ('QA_STAFF', 'QA Staff'),
10
+        ('QA_MANAGER', 'QA Manager'),
11
+    ]
12
+    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
13
+    bio = models.TextField(blank=True, null=True)
14
+    profile_picture = models.ImageField(upload_to="profile/%Y/%m/%d/", blank=True, null=True)
15
+    signed_picture = models.ImageField(upload_to="signed/%Y/%m/%d/", blank=True, null=True)
16
+    email = models.EmailField(blank=True, null=True)  # New email field
17
+    position = models.CharField(max_length=20, choices=POSITION_CHOICES, blank=True, null=True)  # New position field
18
+
19
+    def __str__(self):
20
+        return self.user.username

+ 13 - 0
app/sysadmin/signals.py

@@ -0,0 +1,13 @@
1
+from django.db.models.signals import post_save
2
+from django.dispatch import receiver
3
+from django.contrib.auth.models import User
4
+from .models import UserProfile
5
+
6
+@receiver(post_save, sender=User)
7
+def create_user_profile(sender, instance, created, **kwargs):
8
+    if created:
9
+        UserProfile.objects.create(user=instance)
10
+
11
+@receiver(post_save, sender=User)
12
+def save_user_profile(sender, instance, **kwargs):
13
+    instance.profile.save()

+ 94 - 0
app/sysadmin/templates/sysadmin/profile.html

@@ -0,0 +1,94 @@
1
+{% extends "base.html" %}
2
+{% load tailwind_filters %}
3
+
4
+{% block title %}Report Dashboard{% endblock %}
5
+{% block content %}
6
+<div class="container mx-auto px-4 py-8">
7
+<h1 class="text-2xl font-bold text-gray-700 mb-6">Your Profile</h1>
8
+
9
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
10
+            <!-- Username -->
11
+            <div class="flex items-center bg-gray-50 p-4 rounded-lg shadow-sm border">
12
+                <div class="flex-shrink-0 bg-indigo-100 rounded-full h-12 w-12 flex items-center justify-center">
13
+                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
14
+  <path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
15
+</svg>
16
+
17
+                </div>
18
+                <div class="ml-4">
19
+                    <h3 class="text-lg font-medium text-gray-900">Username</h3>
20
+                    <p class="text-gray-600">{{ user.username }}</p>
21
+                </div>
22
+            </div>
23
+
24
+            <!-- Email -->
25
+            <div class="flex items-center bg-gray-50 p-4 rounded-lg shadow-sm border">
26
+                <div class="flex-shrink-0 bg-indigo-100 rounded-full h-12 w-12 flex items-center justify-center">
27
+                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
28
+  <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
29
+</svg>
30
+
31
+                </div>
32
+                <div class="ml-4">
33
+                    <h3 class="text-lg font-medium text-gray-900">Email</h3>
34
+                    <p class="text-gray-600">{{ user.email }}</p>
35
+                </div>
36
+            </div>
37
+        </div>
38
+<form method="POST" enctype="multipart/form-data" class='space-y-4'>
39
+    {% csrf_token %}
40
+              <!-- First Name -->
41
+              <div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
42
+
43
+                <div>
44
+                  <label for="first_name" class="block text-sm font-medium text-gray-700">First Name</label>
45
+                  <input
46
+                      type="text"
47
+                      name="first_name"
48
+                      id="first_name"
49
+                      value="{{ user.first_name }}"
50
+                      class="block w-full mt-2 rounded-md border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
51
+                      />
52
+                </div>
53
+
54
+                <!-- Last Name -->
55
+                <div>
56
+                  <label for="last_name" class="block text-sm font-medium text-gray-700">Last Name</label>
57
+                  <input
58
+                      type="text"
59
+                      name="last_name"
60
+                      id="last_name"
61
+                      value="{{ user.last_name }}"
62
+                      class="block w-full mt-2 rounded-md border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
63
+                      />
64
+                </div>
65
+              </div>
66
+    {% if form.instance.profile_picture %}
67
+    <!-- Show uploaded profile picture -->
68
+    <img src="{{ form.instance.profile_picture.url }}" 
69
+         alt="Profile Picture" 
70
+         class="w-32 h-32 rounded-full mb-4 shadow-md">
71
+    {% else %}
72
+    <!-- Placeholder image if no profile picture -->
73
+    <div class="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 mb-4">
74
+      <span>No Image</span>
75
+    </div>
76
+    {% endif %}
77
+    {{ form.profile_picture }}
78
+    {% if form.instance.signed_picture %}
79
+    <!-- Show uploaded profile picture -->
80
+    <img src="{{ form.instance.signed_picture.url }}" 
81
+         alt="Profile Picture" 
82
+         class="w-32 mb-4 shadow-md">
83
+    {% else %}
84
+    <!-- Placeholder image if no profile picture -->
85
+    <div class="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 mb-4">
86
+      <span>No Image</span>
87
+    </div>
88
+    {% endif %}
89
+    {{ form.signed_picture }}
90
+    {{ form.position | as_crispy_field }}
91
+    <button type="submit" class="px-6 py-2 bg-indigo-600 text-white font-medium rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">Save Changes</button>
92
+</form>
93
+</div>
94
+{% endblock %}

+ 2 - 0
app/sysadmin/urls.py

@@ -7,4 +7,6 @@ urlpatterns = [
7 7
     path('login/', views.login_view, name='login'),
8 8
     path('register/', views.register_view, name='register'),
9 9
     path('logout/', views.logout_view, name='logout'),
10
+    path('profile/', views.profile_view, name='profile'),  # Add profile view URL
11
+
10 12
 ]

+ 32 - 1
app/sysadmin/views.py

@@ -1,7 +1,10 @@
1 1
 # views.py
2 2
 from django.contrib.auth import authenticate, login, logout
3 3
 from django.shortcuts import render, redirect
4
-from .forms import CustomLoginForm, CustomUserCreationForm
4
+from .forms import CustomLoginForm, CustomUserCreationForm, UserProfileForm
5
+from .models import UserProfile
6
+from django.contrib.auth.decorators import login_required
7
+from django.contrib import messages
5 8
 
6 9
 def login_view(request):
7 10
     if request.method == "POST":
@@ -30,3 +33,31 @@ def register_view(request):
30 33
 def logout_view(request):
31 34
     logout(request)  # Logs out the user
32 35
     return redirect('sysadmin:login')  # Redirect to the login page after logout
36
+
37
+@login_required
38
+def profile_view(request):
39
+    try:
40
+        # Get the profile for the current user
41
+        profile = request.user.profile
42
+    except UserProfile.DoesNotExist:
43
+        # Create a profile if it doesn't exist
44
+        profile = UserProfile.objects.create(user=request.user)
45
+
46
+    if request.method == "POST":
47
+        form = UserProfileForm(request.POST, request.FILES, instance=profile)
48
+
49
+        user = request.user
50
+        user.first_name = request.POST.get('first_name', user.first_name)
51
+        user.last_name = request.POST.get('last_name', user.last_name)
52
+
53
+        if form.is_valid():
54
+            form.save()
55
+            user.save()
56
+            messages.success(request, "Profile Updated")
57
+            return redirect('sysadmin:profile')  # Redirect to the profile page after saving
58
+        else:
59
+            messages.error(request, form.errors)
60
+    else:
61
+        form = UserProfileForm(instance=profile)
62
+
63
+    return render(request, 'sysadmin/profile.html', {'form': form})

+ 2 - 2
app/templates/base.html

@@ -7,7 +7,7 @@
7 7
 <head>
8 8
     <meta charset="UTF-8">
9 9
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
10
-    <title>{% block title %}My Django App{% endblock %}</title>
10
+    <title>{% block title %}COI System{% endblock %}</title>
11 11
 
12 12
     <!-- TailwindCSS -->
13 13
     <!--
@@ -39,7 +39,7 @@
39 39
 
40 40
                 {% if user.is_authenticated %}
41 41
                     <!-- Logged-in User -->
42
-                    <span class="text-gray-600 dark:text-gray-300">Hello, {{ user.username }}</span>
42
+                    <a href="{% url 'sysadmin:profile' %}"><span class="text-gray-600 dark:text-gray-300">Hello, {{ user.username }}</span></a>
43 43
                     <a href="{% url 'sysadmin:logout' %}" class="text-blue-500 hover:underline">Logout</a>
44 44
                 {% else %}
45 45
                     <!-- Guest User -->

BIN
hardness.xlsx


+ 3 - 1
test_xlsx2.py

@@ -139,6 +139,8 @@ if __name__ == "__main__":
139 139
         "customer": "Tum Coder",
140 140
         "inspect_date": "2025-01-15",
141 141
         "lot_no": "12345",
142
+        "staff_name":  "Tum 8888",
143
+        "man_name":  "Tum 999",
142 144
         "size": "Large",
143 145
         "pcs": "10 pcs",
144 146
         "spec": "Spec-A",
@@ -149,7 +151,7 @@ if __name__ == "__main__":
149 151
         "dimension_app.d1_act": "33",
150 152
         "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
151 153
         "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
152
-        "dimension_app.spe_acc": False,  # Hide rows 24 to 28 if the prefix is "0"
154
+        "dimension_app.spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
153 155
     }
154 156
 
155 157
 

read more · c1b7ac2dd4 - Gogs: Simplico Git Service
tum 2 gadi atpakaļ
vecāks
revīzija
c1b7ac2dd4

+ 74 - 0
package-lock.json

@@ -26,6 +26,7 @@
26 26
         "ionicons": "^7.0.0",
27 27
         "swiper": "^9.2.0",
28 28
         "vue": "^3.2.45",
29
+        "vue-read-more": "^1.1.1",
29 30
         "vue-router": "^4.1.6"
30 31
       },
31 32
       "devDependencies": {
@@ -37,7 +38,10 @@
37 38
         "eslint": "^8.35.0",
38 39
         "eslint-plugin-vue": "^9.9.0",
39 40
         "jsdom": "^21.1.0",
41
+        "markdown-truncate": "^1.1.0",
42
+        "marked": "^4.3.0",
40 43
         "node-sass": "^8.0.0",
44
+        "readmore-js": "^2.2.1",
41 45
         "sass": "^1.61.0",
42 46
         "sass-loader": "^13.2.2",
43 47
         "typescript": "^4.9.3",
@@ -4982,6 +4986,12 @@
4982 4986
         "node": ">= 10.13.0"
4983 4987
       }
4984 4988
     },
4989
+    "node_modules/jquery": {
4990
+      "version": "3.6.4",
4991
+      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz",
4992
+      "integrity": "sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ==",
4993
+      "dev": true
4994
+    },
4985 4995
     "node_modules/js-base64": {
4986 4996
       "version": "2.6.4",
4987 4997
       "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
@@ -5483,6 +5493,24 @@
5483 5493
         "url": "https://github.com/sponsors/sindresorhus"
5484 5494
       }
5485 5495
     },
5496
+    "node_modules/markdown-truncate": {
5497
+      "version": "1.1.0",
5498
+      "resolved": "https://registry.npmjs.org/markdown-truncate/-/markdown-truncate-1.1.0.tgz",
5499
+      "integrity": "sha512-totIV8266I//yl7sxCW6xv08T28Yva6a1czpnHlE6scwFOr9yoIbLvzeIp64M3MMyOwo3WX4MVQ+lMQHXAP/ow==",
5500
+      "dev": true
5501
+    },
5502
+    "node_modules/marked": {
5503
+      "version": "4.3.0",
5504
+      "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
5505
+      "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
5506
+      "dev": true,
5507
+      "bin": {
5508
+        "marked": "bin/marked.js"
5509
+      },
5510
+      "engines": {
5511
+        "node": ">= 12"
5512
+      }
5513
+    },
5486 5514
     "node_modules/meow": {
5487 5515
       "version": "9.0.0",
5488 5516
       "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
@@ -7011,6 +7039,15 @@
7011 7039
         "node": ">=8.10.0"
7012 7040
       }
7013 7041
     },
7042
+    "node_modules/readmore-js": {
7043
+      "version": "2.2.1",
7044
+      "resolved": "https://registry.npmjs.org/readmore-js/-/readmore-js-2.2.1.tgz",
7045
+      "integrity": "sha512-hbPP0nQpYYkAywCEZ8ozHivvhWyHic37KJ2IXrHES4qzjp0+nmw8R33MeyMAtXBZfXX4Es8cpd5JBVf9qj47+Q==",
7046
+      "dev": true,
7047
+      "dependencies": {
7048
+        "jquery": ">2.1.4"
7049
+      }
7050
+    },
7014 7051
     "node_modules/redent": {
7015 7052
       "version": "3.0.0",
7016 7053
       "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@@ -8537,6 +8574,11 @@
8537 8574
         "node": ">=4.0"
8538 8575
       }
8539 8576
     },
8577
+    "node_modules/vue-read-more": {
8578
+      "version": "1.1.1",
8579
+      "resolved": "https://registry.npmjs.org/vue-read-more/-/vue-read-more-1.1.1.tgz",
8580
+      "integrity": "sha512-FFv0y5Pg1763fsLo6MA5RSpibThNEoJG6teSRH+rRIQe8O0LAeU4juEn6MRHZN0qOwtXOlbOjO8oit7YSd1NcA=="
8581
+    },
8540 8582
     "node_modules/vue-router": {
8541 8583
       "version": "4.1.6",
8542 8584
       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz",
@@ -12595,6 +12637,12 @@
12595 12637
         "supports-color": "^8.0.0"
12596 12638
       }
12597 12639
     },
12640
+    "jquery": {
12641
+      "version": "3.6.4",
12642
+      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz",
12643
+      "integrity": "sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ==",
12644
+      "dev": true
12645
+    },
12598 12646
     "js-base64": {
12599 12647
       "version": "2.6.4",
12600 12648
       "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
@@ -12985,6 +13033,18 @@
12985 13033
       "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
12986 13034
       "dev": true
12987 13035
     },
13036
+    "markdown-truncate": {
13037
+      "version": "1.1.0",
13038
+      "resolved": "https://registry.npmjs.org/markdown-truncate/-/markdown-truncate-1.1.0.tgz",
13039
+      "integrity": "sha512-totIV8266I//yl7sxCW6xv08T28Yva6a1czpnHlE6scwFOr9yoIbLvzeIp64M3MMyOwo3WX4MVQ+lMQHXAP/ow==",
13040
+      "dev": true
13041
+    },
13042
+    "marked": {
13043
+      "version": "4.3.0",
13044
+      "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
13045
+      "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
13046
+      "dev": true
13047
+    },
12988 13048
     "meow": {
12989 13049
       "version": "9.0.0",
12990 13050
       "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
@@ -14160,6 +14220,15 @@
14160 14220
         "picomatch": "^2.2.1"
14161 14221
       }
14162 14222
     },
14223
+    "readmore-js": {
14224
+      "version": "2.2.1",
14225
+      "resolved": "https://registry.npmjs.org/readmore-js/-/readmore-js-2.2.1.tgz",
14226
+      "integrity": "sha512-hbPP0nQpYYkAywCEZ8ozHivvhWyHic37KJ2IXrHES4qzjp0+nmw8R33MeyMAtXBZfXX4Es8cpd5JBVf9qj47+Q==",
14227
+      "dev": true,
14228
+      "requires": {
14229
+        "jquery": ">2.1.4"
14230
+      }
14231
+    },
14163 14232
     "redent": {
14164 14233
       "version": "3.0.0",
14165 14234
       "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@@ -15238,6 +15307,11 @@
15238 15307
         }
15239 15308
       }
15240 15309
     },
15310
+    "vue-read-more": {
15311
+      "version": "1.1.1",
15312
+      "resolved": "https://registry.npmjs.org/vue-read-more/-/vue-read-more-1.1.1.tgz",
15313
+      "integrity": "sha512-FFv0y5Pg1763fsLo6MA5RSpibThNEoJG6teSRH+rRIQe8O0LAeU4juEn6MRHZN0qOwtXOlbOjO8oit7YSd1NcA=="
15314
+    },
15241 15315
     "vue-router": {
15242 15316
       "version": "4.1.6",
15243 15317
       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz",

+ 4 - 0
package.json

@@ -30,6 +30,7 @@
30 30
     "ionicons": "^7.0.0",
31 31
     "swiper": "^9.2.0",
32 32
     "vue": "^3.2.45",
33
+    "vue-read-more": "^1.1.1",
33 34
     "vue-router": "^4.1.6"
34 35
   },
35 36
   "devDependencies": {
@@ -41,7 +42,10 @@
41 42
     "eslint": "^8.35.0",
42 43
     "eslint-plugin-vue": "^9.9.0",
43 44
     "jsdom": "^21.1.0",
45
+    "markdown-truncate": "^1.1.0",
46
+    "marked": "^4.3.0",
44 47
     "node-sass": "^8.0.0",
48
+    "readmore-js": "^2.2.1",
45 49
     "sass": "^1.61.0",
46 50
     "sass-loader": "^13.2.2",
47 51
     "typescript": "^4.9.3",

+ 1 - 1
src/api_config.ts

@@ -1,3 +1,3 @@
1
-export const API_URL = "http://192.168.1.35:8000/api/"
1
+export const API_URL = "http://192.168.100.180:8000/api/"
2 2
 //export const API_URL = "https://kacee.simplico.net/api/"
3 3
 

+ 26 - 5
src/components/ProductDetail.vue

@@ -33,11 +33,14 @@
33 33
             </ion-col>
34 34
         </ion-row>
35 35
       </ion-grid>
36
-      <ion-text>
37
-        <h1>{{ product.name }}</h1>
38
-        <vue-showdown :markdown="product.description" />
39
- 
40
-      </ion-text>
36
+      <h1>{{ product.name }} {{ product.id }}</h1>
37
+      <!--
38
+        {{ readmore }} -->
39
+      <ion-text v-html="short_desc" v-if="!readmore" />
40
+      
41
+      <ion-text v-html="description" v-if="readmore" />
42
+      <ion-button expand="full" @click="readmore = true" v-if="!readmore" color="medium">Read More</ion-button>
43
+      <ion-button expand="full" @click="readmore = false" v-if="readmore" color="medium">Read Less</ion-button>
41 44
     </template>
42 45
   </ion-content>
43 46
 </template>
@@ -64,10 +67,15 @@ import {
64 67
 } from '@ionic/vue';
65 68
 
66 69
 import { VueShowdown } from 'vue-showdown';
70
+import { ReadMore } from 'vue-read-more';
67 71
 import { ref, onMounted } from 'vue'
68 72
 
69 73
 import axios from 'axios';
70 74
 import { API_URL } from '@/api_config';
75
+import { marked }  from 'marked';
76
+import truncateMarkdown from 'markdown-truncate'
77
+
78
+
71 79
 
72 80
 import { Swiper, SwiperSlide } from 'swiper/vue';
73 81
 import 'swiper/css';
@@ -76,6 +84,10 @@ import '@ionic/vue/css/ionic-swiper.css';
76 84
 const props = defineProps(['pid'])
77 85
 let pid = ref(0)
78 86
 let product = ref(null)
87
+let description = ref(null)
88
+let short_desc = ref(null)
89
+let readmore = ref(false)
90
+
79 91
 //console.log(pid)
80 92
 
81 93
 let name = ref("xxx")
@@ -92,6 +104,15 @@ onMounted(async () => {
92 104
   const res = await axios.get(API_URL + `fn_products/${props.pid}/?format=json`)
93 105
   product.value = res.data
94 106
   pid.value  = props.pid
107
+  console.log("description")
108
+  console.log(description)
109
+  //readmore(description)
110
+  console.log(res.data)
111
+  description.value = marked(res.data.description)
112
+  short_desc.value = truncateMarkdown(res.data.description, { limit: 300, ellipsis: true })
113
+  let sd  = truncateMarkdown(res.data.description, { limit: 300, ellipsis: true })
114
+  short_desc.value = marked(sd)
115
+  console.log(description.value)
95 116
 })
96 117
 
97 118
 </script>

+ 1 - 0
src/views/ProductListPage.vue

@@ -49,6 +49,7 @@ onMounted( async() => {
49 49
   console.log(cat)
50 50
   const res = await axios.get(API_URL+`fn_products/?format=json&cat=${cat}`)
51 51
   prods.value = res.data.results
52
+  console.log("prods")
52 53
   console.log(prods)
53 54
 })
54 55