span>
+                        if isinstance(value, ImageFieldFile):
188
+                            pprint("ImageField")
189
+                            image_path = value.path
190
+                            if os.path.exists(image_path):
191
+                                # img = Image(image_path)
192
+                                # img.height = 40  # Adjust size as needed
193
+                                pil_img = PILImage.open(image_path)
194
+                                original_width, original_height = pil_img.size
195
+
196
+                                # Desired height (e.g., 40), calculate the new width to maintain aspect ratio
197
+                                desired_height = 40
198
+                                aspect_ratio = original_width / original_height
199
+                                new_width = int(desired_height * aspect_ratio)
200
+
201
+                                # Resize the image using Pillow (optional, for saving memory during export)
202
+                                resized_img = pil_img.resize((new_width, desired_height), PILImage.Resampling.LANCZOS)
203
+                                resized_img.save(image_path)  # Save the resized image back to the same path
204
+
205
+                                # Insert the resized image into the Excel sheet
206
+                                img = Image(image_path)
207
+                                img.width, img.height = new_width, desired_height  # Set the dimensions
208
+                                # sheet.add_image(img, cell.coordinate)
209
+                                center_image_in_cell(sheet, img, cell.coordinate, )
210
+                                cell.value = None  # Clear placeholder
211
+
212
+                        elif value is True:
213
+                            img = Image(checked_image_path)
214
+                            img.width = img.height = 10
215
+                            print(f"{cell.coordinate}")
216
+                            # sheet.add_image(img, cell.coordinate)
217
+                            # set_image_with_offset(sheet, img, cell.coordinate, offset_x=100)
218
+                            center_image_in_cell(sheet,img, cell.coordinate, )
219
+                            cell.value = None  # Remove the placeholder text
220
+                        elif value is False:
221
+                            img = Image(unchecked_image_path)
222
+                            img.width = img.height = 10
223
+                            # sheet.add_image(img, cell.coordinate)
224
+                            # set_image_with_offset(sheet, img, cell.coordinate, offset_x=100)
225
+                            center_image_in_cell(sheet, img, cell.coordinate, )
226
+                            cell.value = None  # Remove the placeholder text
227
+                        else:
228
+                            # Insert the text value directly
229
+                            cell.value = value
230
+
231
+        for key, value in data.items():
232
+            if isinstance(value, str) and re.match(r"^\d+\[\d+:\d+\]$", value):
233
+                # Parse the prefix and row range
234
+                prefix, row_range = value.split("[")
235
+                row_start, row_end = map(int, row_range[:-1].split(":"))
236
+                
237
+                # Hide rows if the prefix matches the condition
238
+                if prefix == "0":  # Adjust the condition as needed
239
+                    sheet.row_dimensions.group(row_start, row_end, hidden=True)
240
+        
241
+
242
+    # Generate the output filename with a timestamp
243
+    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
244
+    output_path = f"{prefix_filename}_{timestamp}.xlsx"
245
+    workbook.save(output_path)
246
+
247
+    return output_path
248
+
249
+if __name__ == "__main__":
250
+    # Example usage
251
+    data = {
252
+        "customer": "Tum Coder",
253
+        "inspect_date": "2025-01-15",
254
+        "lot_no": "12345",
255
+        "staff_name":  "Tum 8888",
256
+        "man_name":  "Tum 999",
257
+        "size": "Large",
258
+        "pcs": "10 pcs",
259
+        "spec": "Spec-A",
260
+        "hardness.d1_act": "10",
261
+        "hardness.d2_act": "0[24:28]",  # Hide rows 24 to 28 if the prefix is "0"
262
+        "hardness.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
263
+        "hardness.spe_acc": False,  # Hide rows 24 to 28 if the prefix is "0"
264
+        "dimension_app.d1_act": "33",
265
+        "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
266
+        "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
267
+        "dimension_app.spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
268
+    }
269
+
270
+
271
+
272
+
273
+    output_file = gen_xlsx(
274
+        template_file="./hardness.xlsx",
275
+        selected_sheets=["hardness", "dimension_app"],  # Replace with your actual sheet names
276
+        prefix_filename="./output/output",
277
+        data=data
278
+    )
279
+
280
+    print(f"Generated file: {output_file}")

+ 98 - 45
app/report/templates/report/coi.html

@@ -3,66 +3,63 @@
3 3
 {% block title %}Report Dashboard{% endblock %}
4 4
 
5 5
 {% block content %}
6
-<div class="container mx-auto px-4 py-8">
6
+<div class="container mx-auto px-4 py-8" x-data="COIReport">
7 7
 
8 8
   <h1 class="text-2xl font-bold text-gray-800">Export Center</h1>
9 9
   <form method='post'>
10 10
     {% csrf_token %}
11
-    <div class="flex items-center justify-between mb-4">
12
-      <h1 class="text-lg font-bold text-gray-800">TKX Certificate Issue</h1>
13
-      <button class="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400">
14
-        รายละเอียด CheckedBy|ApproveBy
15
-      </button>
16
-    </div>
17 11
     <div class="flex items-center gap-2 mb-4">
18 12
       <label for="lot-number" class="text-gray-700 font-medium">Lot No. :</label>
19
-      <input id="lot-number" type="text" class="border border-gray-300 rounded px-4 py-2 focus:outline-blue-500" placeholder="Enter Lot No." name='lot_no' required>
13
+      <input id="lot-number" type="text" class="border border-gray-300 rounded px-4 py-2 focus:outline-blue-500" placeholder="Enter Lot No." name='lot_no' required x-model="lot_no">
20 14
       <button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" type="submit" name="search_lot">
21 15
         🔍
22 16
       </button>
23 17
     </div>
24
-    <div class="flex items-center gap-4 mb-4">
25 18
       <button class="bg-blue-100 text-blue-700 px-4 py-2 rounded hover:bg-blue-200">
26 19
         Option for Export :
27 20
       </button>
28
-      <div class="flex items-center space-x-2">
29
-        <label class="flex items-center space-x-1">
30
-          <input type="checkbox" class="rounded" name='exports' value='size_dimension'>
31
-          <span>Size Dimension</span>
32
-        </label>
33
-        <label class="flex items-center space-x-1">
34
-          <input type="checkbox" class="rounded" name='exports' value='grade'>
35
-          <span>Grade</span>
36
-        </label>
21
+        <div class="grid grid-cols-4 gap-4">
22
+        {% for key,value in SHEET_NAMES.items %}
37 23
         <label class="flex items-center space-x-1">
38
-          <input type="checkbox" class="rounded" name='exports' value='wb'>
39
-          <span>WB</span>
24
+          <input type="checkbox" class="rounded" name='exports' value='{{ key }}' x-model='exports'>
25
+          <span>{{ value }}</span>
40 26
         </label>
41
-        <label class="flex items-center space-x-1">
42
-          <input type="checkbox" class="rounded" name='exports' value='fg_weight'>
43
-          <span>FG Weight</span>
44
-        </label>
45
-        <label class="flex items-center space-x-1">
46
-          <input type="checkbox" class="rounded" name='exports' value='centering'>
47
-          <span>Centering</span>
48
-        </label>
49
-        <label class="flex items-center space-x-1">
50
-          <input type="checkbox" class="rounded" name='exports' value='ring_test'>
51
-          <span>Ring Test</span>
52
-        </label>
53
-        <label class="flex items-center space-x-1">
54
-          <input type="checkbox" class="rounded" name='exports' value='rotate'>
55
-          <span>Rotate</span>
56
-        </label>
57
-        <label class="flex items-center space-x-1">
58
-          <input type="checkbox" class="rounded" name='exports' value='final_judge'>
59
-          <span>Final Judge</span>
60
-        </label>
61
-      </div>
62
-      <button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" type='submit' name='export'>
63
-        Export Excel
64
-      </button>
65
-    </div>
27
+        {% endfor %}
28
+        </div>
29
+        <div class="grid grid-cols-2 gap-4">
30
+          <div class="my-4">
31
+            <label for="qa1" class="block mb-2 text-sm font-medium text-gray-900">Select QA.1</label>
32
+            <select id="qa1" name="qa1" class="block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" x-model='qa1'>
33
+              <option value="" disabled selected>Choose a user</option>
34
+              {% for user in users %}
35
+              <option value="{{ user.id }}">{{ user.profile }}</option>
36
+              {% endfor %}
37
+            </select>
38
+          </div>
39
+          <div class="my-4">
40
+            <label for="qa2" class="block mb-2 text-sm font-medium text-gray-900">Select QA.2</label>
41
+            <select id="qa2" name="qa2" class="block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" x-model='qa2'>
42
+              <option value="" disabled selected>Choose a user</option>
43
+              {% for user in users %}
44
+              <option value="{{ user.id }}">{{ user.profile }}</option>
45
+              {% endfor %}
46
+            </select>
47
+          </div>
48
+        </div>
49
+        <div class="flex justify-end my-3 space-x-4">
50
+          <div>
51
+            <button  type='button' class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" 
52
+                                   @click="exportCOI">
53
+              Export Excel
54
+            </button>
55
+          </div>
56
+          <!-- TODO: add download here -->
57
+          <div class=" text-center" x-show="downloadUrl">
58
+            <a :href="downloadUrl"  class="block px-4 py-2 bg-green-600 text-white  rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2" download>
59
+              Download Report
60
+            </a>
61
+          </div>
62
+        </div>
66 63
   </form>
67 64
   {{ result }}
68 65
   
@@ -130,3 +127,59 @@
130 127
 <div class="border-t border-gray-300 my-4"></div>
131 128
 {% include "report/_cen.html" %}
132 129
 {% endblock %}
130
+{% block footer_script %}
131
+<script type="text/javascript">
132
+  function COIReport() {
133
+    return {
134
+      lot_no: '', // Bind this to the input value
135
+      exports: [],
136
+      qa1: null,
137
+      qa2: null,
138
+      gen_report_url: '{% url "report:gen_report" %}', 
139
+      downloadUrl: null, // Stores the download link after export success
140
+      init() {
141
+        //alert("COI Report");
142
+        this.qa1 = '';
143
+        this.qa2 = '';
144
+      },
145
+      async exportCOI() {
146
+              if (!this.lot_no) {
147
+                  alert("Please enter a Lot No.");
148
+                  return;
149
+              }
150
+
151
+              try {
152
+                  // Make POST request using Axios
153
+                  const response = await axios.post(this.gen_report_url, {
154
+                      lot_no: this.lot_no,
155
+                      exports: this.exports,
156
+                      qa1: this.qa1,
157
+                      qa2: this.qa2
158
+                  });
159
+
160
+                  if (response.status === 200) {
161
+                      const result = response.data;
162
+                      alert(`Report generated successfully: ${result.file_url}`);
163
+                      console.log('File URL:', result.file_url);
164
+                      this.downloadUrl = result.file_url; // Set the download URL
165
+                  }
166
+              } catch (error) {
167
+                  if (error.response) {
168
+                      // Server responded with a status other than 2xx
169
+                      alert(`Error: ${error.response.data.message || 'Failed to generate report'}`);
170
+                      console.error('Error response:', error.response);
171
+                  } else if (error.request) {
172
+                      // Request was made but no response received
173
+                      alert('No response from server.');
174
+                      console.error('Request error:', error.request);
175
+                  } else {
176
+                      // Something else caused the error
177
+                      alert('An unexpected error occurred.');
178
+                      console.error('Unexpected error:', error.message);
179
+                  }
180
+              }
181
+          },
182
+    };
183
+  }
184
+</script>
185
+{% endblock %}

+ 2 - 0
app/report/urls.py

@@ -14,6 +14,8 @@ urlpatterns = [
14 14
     path('report/<int:pk>/update/', report_crud.get_update_view().as_view(), name='report-update'),
15 15
     path('report/<int:pk>/delete/', report_crud.get_delete_view().as_view(), name='report-delete'),
16 16
     path('coi/', views.coi_view, name='coi-view'),
17
+    path('report/generate/', views.gen_report_view, name='gen_report'),
18
+
17 19
     # path('create/', views.create_report, name='create'),  # Create a new report
18 20
     # path('<int:pk>/', views.detail_report, name='detail'),  # View details of a specific report
19 21
     # path('<int:pk>/update/', views.update_report, name='update'),  # Update a specific report

+ 223 - 3
app/report/views.py

@@ -8,6 +8,16 @@ 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
+from django.views.decorators.csrf import csrf_exempt
15
+from django.http import JsonResponse, HttpResponseBadRequest
16
+import json
17
+from django.contrib.auth.decorators import login_required
18
+from django.contrib.auth.models import User
19
+from legacy.models import Data
20
+
11 21
 
12 22
 def index(request):
13 23
     reports = Report.objects.all()
@@ -58,9 +68,183 @@ class ReportCRUDView(ConfigurableCRUDView):
58 68
     # config_edit_fields = ["lot_no", "code"]
59 69
     ordering = ["-created_at", "-id",]
60 70
 
71
+def convert_sheet_data(sheet_data):
72
+    """
73
+    Convert sheet_data to the required form with prefixed keys.
74
+    
75
+    :param sheet_data: Dictionary with sheet names as keys and their data as values.
76
+    :return: Dictionary in the required key-value format.
77
+    """
78
+    converted_data = {}
79
+
80
+    for sheet_name, data in sheet_data.items():
81
+        for key, value in data.items():
82
+            # Prefix each key with the sheet name
83
+            converted_key = f"{sheet_name}.{key}"
84
+            converted_data[converted_key] = value
85
+
86
+    return converted_data
87
+
88
+def generate_hardness_out_values(lot_no):
89
+    """
90
+    Generate a dictionary of placeholder values for a given lot_no.
91
+
92
+    :param lot_no: The lot number to query data for.
93
+    :return: A dictionary with placeholders (e.g., v1_1, v1_2, ...) as keys and their respective values.
94
+    """
95
+    # Query the Data model for records matching the given lot_no
96
+    records = Data.objects.filter(lot_no=lot_no).order_by('row_no')
97
+    print(f"records {lot_no} = {records.values()}")
98
+    # Initialize an empty dictionary to store placeholder values
99
+    placeholders = {}
100
+
101
+    # Iterate over the records to populate placeholder values
102
+    for record_idx, record in enumerate(records, start=1):
103
+        placeholders[f'v{record_idx}_1'] = record.p1  # Checkpoint 1 value
104
+        placeholders[f'v{record_idx}_2'] = record.p2  # Checkpoint 2 value
105
+        placeholders[f'v{record_idx}_3'] = record.p3  # Checkpoint 3 value
106
+        placeholders[f'v{record_idx}_4'] = record.avg  # Average value
107
+        placeholders[f'v{record_idx}_5'] = record.rgrade  # Judgment value
108
+
109
+    return placeholders
110
+
111
+def merge_sheet_data_with_data(sheet_data, data):
112
+    """
113
+    Merge `sheet_data` with `data`.
114
+
115
+    :param sheet_data: Dictionary containing the sheet-specific data.
116
+    :param data: Dictionary containing general data.
117
+    :return: A merged dictionary combining both `sheet_data` and `data`.
118
+    """
119
+    # Merge dictionaries using unpacking
120
+    merged_data = {**data, **sheet_data}
121
+
122
+    return merged_data
123
+
124
+def create_coi_file(lot_no, sheets, user, md):
125
+    pprint("---- create_coi_file ---")
126
+    pprint(md)
127
+    qa1 = User.objects.get(pk=md['qa1'])
128
+    qa2 = User.objects.get(pk=md['qa2'])
129
+
130
+    pprint(qa1)
131
+    pprint(qa2)
132
+    
133
+    sheet_data = {}
134
+    for sheet_name in sheets:
135
+        if sheet_name == 'hardness_out':
136
+            sheet_data[sheet_name] = generate_hardness_out_values(lot_no)
137
+    converted_data = convert_sheet_data(sheet_data)
138
+
139
+    print(f"sheet_data \n {sheet_data}") 
140
+    print(f"converted_data \n {converted_data}")
141
+
142
+    data = {
143
+        "customer": "Tum Coder",
144
+        "inspect_date": "2025-01-15",
145
+        "lot_no": "12345",
146
+        "staff_name":  "Tum 8888",
147
+        "man_name":  "Tum 999",
148
+        "size": "Large",
149
+        "pcs": "10 pcs",
150
+        "spec": "Spec-A",
151
+        "hardness_out.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
152
+        "hardness_out.spe_acc": False,  # Hide rows 24 to 28 if the prefix is "0"
153
+        "acc": True,  # Hide rows 24 to 28 if the prefix is "0"
154
+        "spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
155
+        # "hardness_out.qa1": f"{qa1.first_name} {qa1.last_name}",
156
+        # "hardness_out.qa2": f"{qa2.first_name} {qa2.last_name}",
157
+        "qa1": f"{qa1.first_name} {qa1.last_name}",
158
+        "qa2": f"{qa2.first_name} {qa2.last_name}",
159
+        "dimension_app.d1_act": "33",
160
+        "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
161
+        "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
162
+        "dimension_app.spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
163
+        "sign1": qa1.profile.signed_picture,
164
+        "sign2": qa2.profile.signed_picture,
165
+        "pos1": qa1.profile.get_position_display(),
166
+        "pos2": qa2.profile.get_position_display()
167
+    }
168
+    merged_data = merge_sheet_data_with_data(converted_data, data)
169
+    pprint(f"---- merged_data ---")
170
+    pprint(merged_data)
171
+
172
+    output_file = gen_xlsx(
173
+        template_file="/app/report/coi_templates.xlsx",
174
+        selected_sheets=sheets,  # Replace with your actual sheet names
175
+        prefix_filename="/app/media/coi",
176
+        data=merged_data
177
+    )
178
+    report = Report.objects.create(
179
+        name=lot_no,
180
+        created_by=user,
181
+        file=None  # Leave this as None or assign a file if required
182
+    )
183
+    output_file_path = Path(output_file)  # Convert to a Path object for convenience
184
+    with open(output_file_path, "rb") as f:
185
+        report.file.save(output_file_path.name, File(f), save=True)
186
+
187
+    pprint(f"outputfile = {output_file}")
188
+    return report
189
+
190
+SHEET_NAMES = {
191
+    'hardness_out': 'Hardness Out',
192
+    'hardness_out_in': 'Hardness Out/In', 
193
+    'hardness_both_size': 'Hardness Both Size',
194
+    'dimension': 'Dimension',
195
+    'dimension_app': 'Dimension Appearance',
196
+    'dimension_bal_weight': 'Dimension Balance/Weight',
197
+    'dim_bal_app_hard': 'Dimension Balance/Appearance/Hardness',
198
+    'dim_bal_app_rot_hard': 'Dimension Balance/Appearance/Rotation/Hardness',
199
+    'thickness_8_point': 'Thickness 8 Points',
200
+    'centering': 'Centering',
201
+}
61 202
 def coi_view(request):
62 203
     pprint(f"xxxx method = xxx {request.method}")
204
+    users = User.objects.all()
205
+
63 206
     if request.method == "POST":
207
+        pprint(request.POST)
208
+        exports = request.POST.getlist("exports")  # Retrieve the list of selected values
209
+        pprint(f"Selected Export Options: {exports}")
210
+        
211
+        if 'export' in request.POST:
212
+
213
+            data = {
214
+                "customer": "Tum Coder",
215
+                "inspect_date": "2025-01-15",
216
+                "lot_no": "12345",
217
+                "staff_name":  "Tum 8888",
218
+                "man_name":  "Tum 999",
219
+                "size": "Large",
220
+                "lot_size": "10 pcs",
221
+                "spec": "Spec-A",
222
+                "hardness_out.d1_act": "10",
223
+                "hardness_out.d2_act": "0[24:28]",  # Hide rows 24 to 28 if the prefix is "0"
224
+                "hardness_out.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
225
+                "hardness_out.spe_acc": False,  # Hide rows 24 to 28 if the prefix is "0"
226
+                "dimension_app.d1_act": "33",
227
+                "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
228
+                "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
229
+                "dimension_app.spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
230
+            }
231
+            output_file = gen_xlsx(
232
+                template_file="/app/report/coi_templates.xlsx",
233
+                selected_sheets=exports,  # Replace with your actual sheet names
234
+                prefix_filename="/app/media/coi",
235
+                data=data
236
+            )
237
+            report = Report.objects.create(
238
+                name=request.POST.get('lot_no','Untitled'),
239
+                created_by=request.user,
240
+                file=None  # Leave this as None or assign a file if required
241
+            )
242
+            output_file_path = Path(output_file)  # Convert to a Path object for convenience
243
+            with open(output_file_path, "rb") as f:
244
+                report.file.save(output_file_path.name, File(f), save=True)
245
+
246
+            pprint(f"outputfile = {output_file}")
247
+
64 248
         if 'search_lot' in request.POST:
65 249
             lot_no = request.POST.get('lot_no', None)
66 250
             lot_no = lot_no.strip()
@@ -78,8 +262,44 @@ def coi_view(request):
78 262
                                                            'size_str': size_str,
79 263
                                                            'spec': spec})
80 264
 
81
-        exports = request.POST.getlist("exports")  # Retrieve the list of selected values
82
-        pprint(f"Selected Export Options: {exports}")
83 265
         messages.success(request, "Request Sent")
84 266
         return redirect(request.path_info)
85
-    return render(request, 'report/coi.html')
267
+    return render(request, 'report/coi.html', {'SHEET_NAMES': SHEET_NAMES, 'users': users})
268
+
269
+
270
+@csrf_exempt  # Disable CSRF for API requests (ensure this is secure in production)
271
+@login_required
272
+def gen_report_view(request):
273
+    if request.method == "POST":
274
+        try:
275
+            # Parse JSON data from the request body
276
+            data = json.loads(request.body)
277
+            lot_no = data.get("lot_no").strip()
278
+            exports = data.get("exports")
279
+            qa1 = data.get('qa1')
280
+            qa2 = data.get('qa2')
281
+            print(f"data = {data}")
282
+
283
+            if not lot_no:
284
+                return HttpResponseBadRequest("Missing 'lot_no' in request data")
285
+
286
+            # Call the `create_coi_file` function with the provided lot_no
287
+            report = create_coi_file(lot_no, exports, request.user, {'qa1': qa1, 'qa2': qa2})
288
+
289
+            # Return a success response with the report details
290
+            return JsonResponse({
291
+                "message": "Report generated successfully",
292
+                "report_id": report.id,
293
+                "file_url": report.file.url if report.file else None,
294
+            })
295
+
296
+        except json.JSONDecodeError:
297
+            return HttpResponseBadRequest("Invalid JSON data")
298
+        except Exception as e:
299
+            pprint(e)
300
+            return JsonResponse({"error": str(e)}, status=500)
301
+    else:
302
+        return HttpResponseBadRequest("Only POST requests are allowed")
303
+
304
+
305
+

~$hardness.xlsx → app/report/~$coi_templates.xlsx


+ 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

+ 12 - 0
app/sysadmin/filters.py

@@ -0,0 +1,12 @@
1
+
2
+import django_filters
3
+from django.contrib.auth.models import User
4
+
5
+class UserFilter(django_filters.FilterSet):
6
+    username = django_filters.CharFilter(field_name='username', lookup_expr='icontains')
7
+    first_name = django_filters.CharFilter(field_name='first_name', lookup_expr='icontains')
8
+    last_name = django_filters.CharFilter(field_name='last_name', lookup_expr='icontains')
9
+
10
+    class Meta:
11
+        model = User
12
+        fields = ['username', 'first_name', 'last_name']  # Add fields you want to filter

+ 61 - 2
app/sysadmin/forms.py

@@ -2,6 +2,11 @@
2 2
 from django import forms
3 3
 from django.contrib.auth.forms import AuthenticationForm
4 4
 
5
+from django.contrib.auth.forms import UserCreationForm
6
+from django.contrib.auth.models import User
7
+
8
+from .models import UserProfile
9
+
5 10
 class CustomLoginForm(AuthenticationForm):
6 11
     username = forms.CharField(widget=forms.TextInput(attrs={
7 12
         'placeholder': 'Username'
@@ -11,8 +16,6 @@ class CustomLoginForm(AuthenticationForm):
11 16
     }))
12 17
 
13 18
 # forms.py
14
-from django.contrib.auth.forms import UserCreationForm
15
-from django.contrib.auth.models import User
16 19
 
17 20
 class CustomUserCreationForm(UserCreationForm):
18 21
     class Meta:
@@ -33,3 +36,59 @@ class CustomUserCreationForm(UserCreationForm):
33 36
         self.fields['password2'].widget.attrs.update({
34 37
             'placeholder': 'Confirm Password'
35 38
         })
39
+
40
+
41
+class UserProfileForm(forms.ModelForm):
42
+    class Meta:
43
+        model = UserProfile
44
+        fields = ['profile_picture', 'position', 'signed_picture']  # Include the fields you want to manage
45
+
46
+class UserCustomForm(forms.ModelForm):
47
+    # Profile fields
48
+    profile_picture = forms.ImageField(required=False, label="Profile Picture")
49
+    signed_picture = forms.ImageField(required=False, label="Signed Picture")
50
+    position = forms.ChoiceField(
51
+        choices=UserProfile.POSITION_CHOICES, 
52
+        required=False, 
53
+        label="Position"
54
+    )
55
+
56
+    class Meta:
57
+        model = User
58
+        fields = [
59
+            "first_name",
60
+            "last_name",
61
+            "is_staff",
62
+            "is_superuser",
63
+            "is_active",
64
+        ]
65
+
66
+    def __init__(self, *args, **kwargs):
67
+        # Allow passing `instance` to access both User and UserProfile objects
68
+        user_instance = kwargs.pop('instance', None)
69
+        profile_instance = getattr(user_instance, 'profile', None)
70
+        super().__init__(instance=user_instance, *args, **kwargs)
71
+
72
+        # Populate initial data for profile fields if `profile` exists
73
+        if profile_instance:
74
+            self.fields['profile_picture'].initial = profile_instance.profile_picture
75
+            self.fields['signed_picture'].initial = profile_instance.signed_picture
76
+            self.fields['position'].initial = profile_instance.position
77
+
78
+    def save(self, commit=True):
79
+        # Save the User instance first
80
+        user = super().save(commit=commit)
81
+        profile_data = {
82
+            'profile_picture': self.cleaned_data.get('profile_picture'),
83
+            'signed_picture': self.cleaned_data.get('signed_picture'),
84
+            'position': self.cleaned_data.get('position'),
85
+        }
86
+
87
+        # Ensure profile exists for the user
88
+        profile, created = UserProfile.objects.get_or_create(user=user)
89
+        for key, value in profile_data.items():
90
+            setattr(profile, key, value)
91
+        if commit:
92
+            profile.save()
93
+
94
+        return user

+ 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
+    ]

+ 20 - 0
app/sysadmin/models.py

@@ -1,3 +1,23 @@
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. MG.'),
11
+        ('QA_AST_MANAGER', 'QA. Asst. MG.'),
12
+        ('QA_ENGINEER', 'QA. Engineer'),
13
+    ]
14
+    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
15
+    bio = models.TextField(blank=True, null=True)
16
+    profile_picture = models.ImageField(upload_to="profile/%Y/%m/%d/", blank=True, null=True)
17
+    signed_picture = models.ImageField(upload_to="signed/%Y/%m/%d/", blank=True, null=True)
18
+    email = models.EmailField(blank=True, null=True)  # New email field
19
+    position = models.CharField(max_length=20, choices=POSITION_CHOICES, blank=True, null=True)  # New position field
20
+
21
+    def __str__(self):
22
+        pos = self.get_position_display()
23
+        return f"{self.user.username} / {self.user.first_name} {self.user.last_name} #{pos}"

+ 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 %}

+ 71 - 0
app/sysadmin/templates/sysadmin/user_form.html

@@ -0,0 +1,71 @@
1
+{% extends "base.html" %}
2
+
3
+{% load legacy_filters %}
4
+{% load tailwind_filters %}
5
+{% load crispy_forms_filters %}
6
+
7
+{% block title %}
8
+    {% if view.title %}
9
+        {{ view.title }}
10
+    {% else %}
11
+        {{ view|class_name }}
12
+    {% endif %}
13
+{% endblock %}
14
+
15
+{% block content %}
16
+<div class="container mx-auto px-4 py-6">
17
+    <h1 class="text-2xl font-bold mb-6">
18
+        {% if view.title %}
19
+            {{ view.title }}
20
+        {% elif view|class_name == "CreateViewClass" %}
21
+            Create {{ model_verbose_name }}
22
+        {% else %}
23
+            Update {{ model_verbose_name }}
24
+        {% endif %}
25
+    </h1>
26
+
27
+    <!-- Render the Form -->
28
+    <form method="post" enctype="multipart/form-data">
29
+        {% csrf_token %}
30
+        <div class="flex flex-wrap items-center space-x-4">
31
+            <!-- Show profile_picture -->
32
+            <div class=" mb-4">
33
+                {% if form.instance.profile and form.instance.profile.profile_picture %}
34
+                    <div class="mb-2">
35
+                        <img src="{{ form.instance.profile.profile_picture.url }}" alt="Profile Picture" class="max-w-xs border rounded" width=100>
36
+                    </div>
37
+                {% endif %}
38
+                {{ form.profile_picture|as_crispy_field }}
39
+            </div>
40
+
41
+            <!-- Show signed_picture -->
42
+            <div class=" mb-4">
43
+                {% if form.instance.profile and form.instance.profile.signed_picture %}
44
+                    <div class="mb-2">
45
+                        <img src="{{ form.instance.profile.signed_picture.url }}" alt="Signed Picture" class="max-w-xs border rounded" width=100>
46
+                    </div>
47
+                {% endif %}
48
+                {{ form.signed_picture|as_crispy_field }}
49
+            </div>
50
+
51
+            <!-- Render other form fields -->
52
+            {% for field in form %}
53
+                {% if field.name not in "profile_picture signed_picture" %}
54
+                    <div class=" mb-4">
55
+                        {{ field|as_crispy_field }}
56
+                    </div>
57
+                {% endif %}
58
+            {% endfor %}
59
+        </div>
60
+
61
+        <div class="mt-4">
62
+            <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
63
+                Save
64
+            </button>
65
+            <a href="{% url list_url_name %}" class="bg-gray-300 text-gray-800 px-4 py-2 rounded hover:bg-gray-400">
66
+                Cancel
67
+            </a>
68
+        </div>
69
+    </form>
70
+</div>
71
+{% endblock %}

+ 8 - 0
app/sysadmin/urls.py

@@ -3,8 +3,16 @@ from . import views
3 3
 
4 4
 app_name = "sysadmin"  # Use this namespace for reverse URL lookups
5 5
 
6
+users_crud = views.UserCRUDView()
7
+
6 8
 urlpatterns = [
7 9
     path('login/', views.login_view, name='login'),
8 10
     path('register/', views.register_view, name='register'),
9 11
     path('logout/', views.logout_view, name='logout'),
12
+    path('profile/', views.profile_view, name='profile'),  # Add profile view URL
13
+
14
+    path('users/', users_crud.get_list_view().as_view(), name='users-list'),
15
+    path('users/create/', users_crud.get_create_view().as_view(), name='users-create'),
16
+    path('users/<int:pk>/update/', users_crud.get_update_view().as_view(), name='users-update'),
17
+    path('users/<int:pk>/delete/', users_crud.get_delete_view().as_view(), name='users-delete'),
10 18
 ]

+ 57 - 1
app/sysadmin/views.py

@@ -1,7 +1,13 @@
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, UserCustomForm
5
+from .models import UserProfile
6
+from .filters import UserFilter
7
+from django.contrib.auth.decorators import login_required
8
+from django.contrib import messages
9
+from core.utils import ConfigurableCRUDView
10
+from django.contrib.auth.models import User
5 11
 
6 12
 def login_view(request):
7 13
     if request.method == "POST":
@@ -30,3 +36,53 @@ def register_view(request):
30 36
 def logout_view(request):
31 37
     logout(request)  # Logs out the user
32 38
     return redirect('sysadmin:login')  # Redirect to the login page after logout
39
+
40
+@login_required
41
+def profile_view(request):
42
+    try:
43
+        # Get the profile for the current user
44
+        profile = request.user.profile
45
+    except UserProfile.DoesNotExist:
46
+        # Create a profile if it doesn't exist
47
+        profile = UserProfile.objects.create(user=request.user)
48
+
49
+    if request.method == "POST":
50
+        form = UserProfileForm(request.POST, request.FILES, instance=profile)
51
+
52
+        user = request.user
53
+        user.first_name = request.POST.get('first_name', user.first_name)
54
+        user.last_name = request.POST.get('last_name', user.last_name)
55
+
56
+        if form.is_valid():
57
+            form.save()
58
+            user.save()
59
+            messages.success(request, "Profile Updated")
60
+            return redirect('sysadmin:profile')  # Redirect to the profile page after saving
61
+        else:
62
+            messages.error(request, form.errors)
63
+    else:
64
+        form = UserProfileForm(instance=profile)
65
+
66
+    return render(request, 'sysadmin/profile.html', {'form': form})
67
+
68
+
69
+class UserCRUDView(ConfigurableCRUDView):
70
+    model = User
71
+    list_template_name = 'legacy/datacrud_list.html'
72
+    detail_template_name = 'legacy/datacrud_detail.html'
73
+    form_template_name = 'sysadmin/user_form.html'
74
+    confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
75
+    filterset_class = UserFilter
76
+
77
+    page_title = "Users"
78
+
79
+    # URL name mappings
80
+    list_url_name = 'sysadmin:users-list'
81
+    create_url_name = 'sysadmin:users-create'
82
+    update_url_name = 'sysadmin:users-update'
83
+    delete_url_name = 'sysadmin:users-delete'
84
+    # excludes = ["splitdata"]
85
+    config_fields = ("id", "username", "is_active", "is_staff", "is_superuser", "last_login")
86
+    config_field_orders = ["id",]  # Display these fields first
87
+    form_class = UserCustomForm
88
+    config_edit_fields = None

+ 44 - 12
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
     <!--
@@ -16,6 +16,7 @@
16 16
     <link href="{% static "font-awesome/css/font-awesome.css" %}" rel="stylesheet" />
17 17
     <script type="text/javascript" defer src="{% static "alpinejs/dist/cdn.min.js" %}"></script>
18 18
     <link href="https://cdn.jsdelivr.net/npm/flowbite@2.5.2/dist/flowbite.min.css" rel="stylesheet" />
19
+    <script type="text/javascript"  src="{% static "axios/dist/axios.js" %}"></script>
19 20
     <script type="text/javascript"  src="{% static "js/main.js" %}"></script>
20 21
 </head>
21 22
 <body class="bg-gray-100 text-gray-800">
@@ -39,7 +40,7 @@
39 40
 
40 41
                 {% if user.is_authenticated %}
41 42
                     <!-- Logged-in User -->
42
-                    <span class="text-gray-600 dark:text-gray-300">Hello, {{ user.username }}</span>
43
+                    <a href="{% url 'sysadmin:profile' %}"><span class="text-gray-600 dark:text-gray-300">Hello, {{ user.username }}</span></a>
43 44
                     <a href="{% url 'sysadmin:logout' %}" class="text-blue-500 hover:underline">Logout</a>
44 45
                 {% else %}
45 46
                     <!-- Guest User -->
@@ -81,6 +82,15 @@
81 82
                             <a href="{% url "legacy:data-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Data</a>
82 83
                           </li>
83 84
                           <li>
85
+                            <a href="{% url "legacy:datams-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Data MS</a>
86
+                          </li>
87
+                          <li>
88
+                            <a href="{% url "legacy:datarl-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Data RL</a>
89
+                          </li>
90
+                          <li>
91
+                            <a href="{% url "legacy:datawb-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Data WB</a>
92
+                          </li>
93
+                          <li>
84 94
                             <a href="{% url "legacy:datams-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Measurement</a>
85 95
                           </li>
86 96
                           <li>
@@ -90,6 +100,12 @@
90 100
                             <a href="{% url "legacy:ls-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Lot Summary</a>
91 101
                           </li>
92 102
                           <li>
103
+                            <a href="{% url "legacy:lsrl-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Lot Summary RL</a>
104
+                          </li>
105
+                          <li>
106
+                            <a href="{% url "legacy:lswb-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Lot Summary WB</a>
107
+                          </li>
108
+                          <li>
93 109
                             <a href="{% url "legacy:vm-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">VMaster</a>
94 110
                           </li>
95 111
                           <li>
@@ -101,12 +117,26 @@
101 117
                           <li>
102 118
                             <a href="{% url "legacy:em-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">EMaster</a>
103 119
                           </li>
104
-                          <li>
105
-                             <a href="#" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Invoice</a>
106
-                          </li>
107 120
                     </ul>
108 121
                  </li>
109
-                <li><a href="/settings/" class="flex items-center p-2 text-gray-900 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white"><span class="ml-3">Settings</span></a></li>
122
+                 <li>
123
+
124
+                   <button type="button" class="flex items-center w-full p-2 text-base text-gray-900 transition duration-75 rounded-lg group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700" aria-controls="setting-sub" data-collapse-toggle="setting-sub">
125
+                     <svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
126
+                       <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 6c0 1.657-3.134 3-7 3S5 7.657 5 6m14 0c0-1.657-3.134-3-7-3S5 4.343 5 6m14 0v6M5 6v6m0 0c0 1.657 3.134 3 7 3s7-1.343 7-3M5 12v6c0 1.657 3.134 3 7 3s7-1.343 7-3v-6"/>
127
+                     </svg>
128
+
129
+                     <span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">Settings</span>
130
+                     <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
131
+                       <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
132
+                     </svg>
133
+                   </button>
134
+                   <ul id="setting-sub" class="hidden py-2 space-y-2">
135
+                     <li>
136
+                       <a href="{% url "sysadmin:users-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Users</a>
137
+                     </li>
138
+                   </ul>
139
+                 </li>
110 140
             </ul>
111 141
         </div>
112 142
     </aside>
@@ -144,14 +174,16 @@
144 174
 
145 175
     {% django_browser_reload_script %}
146 176
     <script>
147
-    // Auto-hide the message after 5 seconds
148
-    setTimeout(() => {
177
+      // Auto-hide the message after 5 seconds
178
+      setTimeout(() => {
149 179
         const alert = document.getElementById('message-alert');
150 180
         if (alert) {
151
-            alert.style.opacity = '0'; // Fade out
152
-            setTimeout(() => alert.remove(), 500); // Remove after fade-out
181
+          alert.style.opacity = '0'; // Fade out
182
+          setTimeout(() => alert.remove(), 500); // Remove after fade-out
153 183
         }
154
-    }, 5000); // 5 seconds
155
-</script>
184
+      }, 5000); // 5 seconds
185
+    </script>
186
+    {% block footer_script %}
187
+    {% endblock %}
156 188
 </body>
157 189
 </html>

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
 

tum/fm99-app - Gogs: Simplico Git Service

1 Commity (master)

Autor SHA1 Wiadomość Data
  tum 1aff8a64ad Initial commit 5 lat temu