tum 1 yıl önce
ebeveyn
işleme
028e97494c

+ 7 - 0
app/core/forms.py

@@ -0,0 +1,7 @@
1
+from django import forms
2
+from .models import Report
3
+
4
+class ReportForm(forms.ModelForm):
5
+    class Meta:
6
+        model = Report
7
+        fields = ['name', 'created_by', 'file']  # Include the fields you want in the form

+ 280 - 0
app/core/migrations/0004_allproductaverageobminmaxview_and_more.py

@@ -0,0 +1,280 @@
1
+# Generated by Django 5.0 on 2025-01-04 15:30
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ('core', '0003_vmasterview'),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.CreateModel(
14
+            name='AllProductAverageObMinMaxView',
15
+            fields=[
16
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17
+                ('ProductCode', models.CharField(max_length=255, null=True)),
18
+                ('out_min', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
19
+                ('out_max', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
20
+                ('in_min', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
21
+                ('in_max', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
22
+            ],
23
+            options={
24
+                'db_table': 'AllProduct_Average_OB_MIN_MAX_view',
25
+                'managed': False,
26
+            },
27
+        ),
28
+        migrations.CreateModel(
29
+            name='AllProductDimensionForInsProcess',
30
+            fields=[
31
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
32
+                ('ProdType', models.CharField(max_length=255, null=True)),
33
+                ('ProductCode', models.CharField(max_length=255, null=True)),
34
+                ('Size_Id', models.CharField(max_length=255, null=True)),
35
+                ('Size_Name', models.CharField(max_length=255, null=True)),
36
+                ('Std', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
37
+                ('TolUn', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
38
+                ('TolUp', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
39
+            ],
40
+            options={
41
+                'db_table': 'AllProduct_Dimension_ForInsProcess',
42
+                'managed': False,
43
+            },
44
+        ),
45
+        migrations.CreateModel(
46
+            name='AllProductPressPositionPressWeight',
47
+            fields=[
48
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
49
+                ('ProductCode', models.CharField(max_length=255, null=True)),
50
+                ('Lot_No', models.CharField(max_length=255, null=True)),
51
+                ('PO_Qty', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
52
+                ('UWeight', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
53
+                ('Current_ProNo', models.CharField(max_length=255, null=True)),
54
+                ('Press_Time', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
55
+                ('PressType_1', models.CharField(max_length=255, null=True)),
56
+                ('PressWeight_1', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
57
+                ('PressType_2', models.CharField(max_length=255, null=True)),
58
+                ('PressWeight_2', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
59
+                ('Press_Ton', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
60
+                ('Press_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
61
+                ('Press_T_Tol', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
62
+                ('Mold_D', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
63
+                ('Mold_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
64
+                ('SegMold_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
65
+                ('SegMold_D', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
66
+                ('Center_D', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
67
+                ('Center_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
68
+                ('LowerPlate_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
69
+                ('StudPlate_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
70
+                ('UpperPlate_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
71
+                ('PinPlate_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
72
+                ('TopConcave_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
73
+                ('BottomConcave_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
74
+            ],
75
+            options={
76
+                'db_table': 'AllProduct_PressPosition_PressWeight',
77
+                'managed': False,
78
+            },
79
+        ),
80
+        migrations.CreateModel(
81
+            name='BelMasterView',
82
+            fields=[
83
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
84
+                ('PRO0', models.CharField(max_length=255, null=True)),
85
+                ('PRO1', models.CharField(max_length=255, null=True)),
86
+                ('PRO1C', models.CharField(max_length=255, null=True)),
87
+                ('PRO2', models.CharField(max_length=255, null=True)),
88
+                ('PRO5', models.CharField(max_length=255, null=True)),
89
+                ('PRO8', models.CharField(max_length=255, null=True)),
90
+                ('PRO9', models.CharField(max_length=255, null=True)),
91
+                ('PRO10', models.CharField(max_length=255, null=True)),
92
+                ('PRO11', models.CharField(max_length=255, null=True)),
93
+                ('PRO12', models.CharField(max_length=255, null=True)),
94
+                ('PRO13', models.CharField(max_length=255, null=True)),
95
+                ('PRO14', models.CharField(max_length=255, null=True)),
96
+                ('PRO15', models.CharField(max_length=255, null=True)),
97
+                ('PRO16', models.CharField(max_length=255, null=True)),
98
+                ('PRO17', models.CharField(max_length=255, null=True)),
99
+                ('PRO18', models.CharField(max_length=255, null=True)),
100
+                ('PRO21', models.CharField(max_length=255, null=True)),
101
+                ('PRO25', models.CharField(max_length=255, null=True)),
102
+                ('PRO27', models.CharField(max_length=255, null=True)),
103
+                ('P2', models.CharField(max_length=255, null=True)),
104
+                ('PRO6', models.CharField(max_length=255, null=True)),
105
+                ('SPEED', models.CharField(max_length=255, null=True)),
106
+                ('PRO_TOOL', models.CharField(max_length=255, null=True)),
107
+                ('PRO4', models.CharField(max_length=255, null=True)),
108
+                ('MC11', models.CharField(max_length=255, null=True)),
109
+                ('MC12', models.CharField(max_length=255, null=True)),
110
+                ('MC14', models.CharField(max_length=255, null=True)),
111
+                ('MC15', models.CharField(max_length=255, null=True)),
112
+                ('MC16', models.CharField(max_length=255, null=True)),
113
+                ('MC19', models.CharField(max_length=255, null=True)),
114
+                ('MC20', models.CharField(max_length=255, null=True)),
115
+                ('MC21', models.CharField(max_length=255, null=True)),
116
+                ('MC22', models.CharField(max_length=255, null=True)),
117
+                ('MC23', models.CharField(max_length=255, null=True)),
118
+                ('MC24', models.CharField(max_length=255, null=True)),
119
+                ('MP45', models.CharField(max_length=255, null=True)),
120
+                ('MP49', models.CharField(max_length=255, null=True)),
121
+                ('MI13', models.CharField(max_length=255, null=True)),
122
+                ('TC', models.CharField(max_length=255, null=True)),
123
+                ('MI14', models.CharField(max_length=255, null=True)),
124
+                ('MI15', models.CharField(max_length=255, null=True)),
125
+                ('MI16', models.CharField(max_length=255, null=True)),
126
+                ('MI17', models.CharField(max_length=255, null=True)),
127
+                ('MI18', models.CharField(max_length=255, null=True)),
128
+                ('MI19', models.CharField(max_length=255, null=True)),
129
+                ('MI20', models.CharField(max_length=255, null=True)),
130
+                ('MI21', models.CharField(max_length=255, null=True)),
131
+                ('MI22', models.CharField(max_length=255, null=True)),
132
+                ('MI23', models.CharField(max_length=255, null=True)),
133
+                ('MI31', models.CharField(max_length=255, null=True)),
134
+                ('MI33', models.CharField(max_length=255, null=True)),
135
+                ('INSAGM', models.CharField(max_length=255, null=True)),
136
+                ('MARAGM', models.CharField(max_length=255, null=True)),
137
+                ('MI53', models.CharField(max_length=255, null=True)),
138
+                ('MI55', models.CharField(max_length=255, null=True)),
139
+                ('MI36', models.CharField(max_length=255, null=True)),
140
+                ('MI39', models.CharField(max_length=255, null=True)),
141
+                ('MI24', models.CharField(max_length=255, null=True)),
142
+                ('Ind1', models.CharField(max_length=255, null=True)),
143
+                ('Ind2', models.CharField(max_length=255, null=True)),
144
+                ('Ind3', models.CharField(max_length=255, null=True)),
145
+                ('AGR1', models.CharField(max_length=255, null=True)),
146
+                ('AGR2', models.CharField(max_length=255, null=True)),
147
+                ('ob_Condition', models.CharField(max_length=255, null=True)),
148
+            ],
149
+            options={
150
+                'db_table': 'bel_master_view',
151
+                'managed': False,
152
+            },
153
+        ),
154
+        migrations.CreateModel(
155
+            name='EMasterView',
156
+            fields=[
157
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
158
+                ('PRO0', models.CharField(max_length=255, null=True)),
159
+                ('PRO1', models.CharField(max_length=255, null=True)),
160
+                ('PRO1C', models.CharField(max_length=255, null=True)),
161
+                ('PRO2', models.CharField(max_length=255, null=True)),
162
+                ('PRO5', models.CharField(max_length=255, null=True)),
163
+                ('PRO8', models.CharField(max_length=255, null=True)),
164
+                ('PRO9', models.CharField(max_length=255, null=True)),
165
+                ('PRO10', models.CharField(max_length=255, null=True)),
166
+                ('PRO11', models.CharField(max_length=255, null=True)),
167
+                ('PRO12', models.CharField(max_length=255, null=True)),
168
+                ('PRO13', models.CharField(max_length=255, null=True)),
169
+                ('PRO14', models.CharField(max_length=255, null=True)),
170
+                ('PRO15', models.CharField(max_length=255, null=True)),
171
+                ('PRO16', models.CharField(max_length=255, null=True)),
172
+                ('PRO17', models.CharField(max_length=255, null=True)),
173
+                ('PRO18', models.CharField(max_length=255, null=True)),
174
+                ('PRO21', models.CharField(max_length=255, null=True)),
175
+                ('PRO25', models.CharField(max_length=255, null=True)),
176
+                ('PRO27', models.CharField(max_length=255, null=True)),
177
+                ('P2', models.CharField(max_length=255, null=True)),
178
+                ('PRO6', models.CharField(max_length=255, null=True)),
179
+                ('SPEED', models.CharField(max_length=255, null=True)),
180
+                ('PRO_TOOL', models.CharField(max_length=255, null=True)),
181
+                ('PRO4', models.CharField(max_length=255, null=True)),
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': 'e_master_view',
225
+                'managed': False,
226
+            },
227
+        ),
228
+        migrations.CreateModel(
229
+            name='VwRotateLog',
230
+            fields=[
231
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
232
+                ('PROD_CODE', models.CharField(max_length=255, null=True)),
233
+                ('LOT_NO', models.CharField(max_length=255, null=True)),
234
+                ('SPEED_SPEC', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
235
+                ('SPEED_TEST', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
236
+                ('SPEED_NG', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
237
+                ('CREATED_DT', models.DateTimeField(null=True)),
238
+                ('LAST_UPD_DT', models.DateTimeField(null=True)),
239
+                ('QTY', models.IntegerField(null=True)),
240
+                ('STATION_NO', models.CharField(max_length=255, null=True)),
241
+            ],
242
+            options={
243
+                'db_table': 'VwRotateLog_Mockup_Data',
244
+                'managed': False,
245
+            },
246
+        ),
247
+        migrations.CreateModel(
248
+            name='VwRotateResult',
249
+            fields=[
250
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
251
+                ('PROD_CODE', models.CharField(max_length=255, null=True)),
252
+                ('LOT_NO', models.CharField(max_length=255, null=True)),
253
+                ('SPEED_SPEC', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
254
+                ('QTY', models.IntegerField(null=True)),
255
+                ('STATION_NO', models.CharField(max_length=255, null=True)),
256
+                ('START_DT', models.DateTimeField(null=True)),
257
+                ('TEST_NO', models.CharField(max_length=255, null=True)),
258
+                ('SPEED_TEST_VAL', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
259
+                ('SPEED_NG_VAL', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
260
+                ('TEST_END_DT', models.DateTimeField(null=True)),
261
+            ],
262
+            options={
263
+                'db_table': 'VW_ROTATE_RESULT',
264
+                'managed': False,
265
+            },
266
+        ),
267
+        migrations.AlterModelOptions(
268
+            name='mgmasterview',
269
+            options={'managed': False},
270
+        ),
271
+        migrations.AlterModelOptions(
272
+            name='vmasterview',
273
+            options={'managed': False},
274
+        ),
275
+        migrations.AddField(
276
+            model_name='report',
277
+            name='file',
278
+            field=models.FileField(blank=True, null=True, upload_to='reports/files/'),
279
+        ),
280
+    ]

+ 2 - 1
app/core/models.py

@@ -12,7 +12,8 @@ class Report(models.Model):
12 12
     )  # Reference to the user who created the report
13 13
     created_at = models.DateTimeField(auto_now_add=True)  # Automatically set when created
14 14
     updated_at = models.DateTimeField(auto_now=True)  # Automatically updated when modified
15
-
15
+    file = models.FileField(upload_to='reports/files/', null=True, blank=True)  # File field
16
+    
16 17
     def __str__(self):
17 18
         return self.name
18 19
 

+ 214 - 0
app/core/utils.py

@@ -0,0 +1,214 @@
1
+from django.shortcuts import render
2
+from django.contrib import messages
3
+# Create your views here.
4
+from django_filters.views import FilterView
5
+from django.urls import reverse_lazy, reverse
6
+from django.views.generic import (
7
+    ListView,
8
+    DetailView,
9
+    CreateView,
10
+    UpdateView,
11
+    DeleteView,
12
+)
13
+from django.core.paginator import Paginator
14
+
15
+class ConfigurableCRUDView:
16
+    model = None
17
+    list_template_name = None
18
+    detail_template_name = None
19
+    form_template_name = None
20
+    confirm_delete_template_name = None
21
+    fields = '__all__'  # Default to all fields
22
+    paginate_by = 10
23
+    filterset_class = None
24
+    page_title = "Page Title"
25
+    create_url_name = None
26
+    update_url_name = None
27
+    delete_url_name = None
28
+    list_url_name = None
29
+    config_fields = "all"  # "all" or a list of field names to display
30
+    config_field_orders = []  # Fields to display first
31
+    config_excludes = []  # Fields to exclude
32
+
33
+    config_readonly_fields = []  # Fields that should be read-only in update view
34
+
35
+    config_edit_fields = "__all__"  # "all" or a list of field names to display
36
+    ordering = None
37
+
38
+
39
+
40
+    def get_fields(self):
41
+        """
42
+        Dynamically generate the fields to be displayed in the list view.
43
+        - Includes all fields if `config_fields` is set to "all".
44
+        - Respects `config_field_orders` and `config_excludes`.
45
+        Returns field instances instead of field names.
46
+        """
47
+        model_fields = {f.name: f for f in self.model._meta.get_fields()}
48
+
49
+        # Filter based on `config_fields` configuration
50
+        fields = []
51
+        if self.config_fields != "all":
52
+            fields = [model_fields[f] for f in self.config_fields if f in model_fields]
53
+        else:
54
+            fields = list(model_fields.values())
55
+        # pprint("------------------------")
56
+        # pprint(f"fields = {fields}")
57
+
58
+        # Exclude fields specified in `config_excludes`
59
+        fields = [f for f in fields if f.name not in self.config_excludes]
60
+
61
+        # Order fields based on `config_field_orders`
62
+        ordered_fields = [f for f in fields if f.name in self.config_field_orders]
63
+        remaining_fields = [f for f in fields if f.name not in self.config_field_orders]
64
+
65
+        # Reorder fields to match the order specified in `config_field_orders`
66
+        ordered_field_names = set(self.config_field_orders)
67
+        ordered_fields.sort(key=lambda f: self.config_field_orders.index(f.name) if f.name in ordered_field_names else len(ordered_field_names))
68
+
69
+        return ordered_fields + remaining_fields
70
+
71
+
72
+    def get_breadcrumbs(self, view_type, obj=None):
73
+        """
74
+        Generate breadcrumbs dynamically based on the view type.
75
+        :param view_type: The type of view (list, create, update, detail, delete)
76
+        :param obj: The current object (if applicable)
77
+        :return: A list of breadcrumb dictionaries
78
+        """
79
+        breadcrumbs = [
80
+            {'label': 'Dashboard', 'url': reverse('dashboard:index')},
81
+            {'label': self.page_title, 'url': reverse(self.list_url_name)} if self.list_url_name else None,
82
+        ]
83
+
84
+        if view_type == 'create':
85
+            breadcrumbs.append({'label': f'Create {self.model._meta.verbose_name}', 'url': None})
86
+        elif view_type == 'update' and obj:
87
+            breadcrumbs.append({'label': obj, 'url': reverse(self.update_url_name, kwargs={'pk': obj.pk})})
88
+        elif view_type == 'detail' and obj:
89
+            breadcrumbs.append({'label': f'Detail of {obj}', 'url': None})
90
+        elif view_type == 'delete' and obj:
91
+            breadcrumbs.append({'label': f'Delete {obj}', 'url': None})
92
+
93
+        return [b for b in breadcrumbs if b]
94
+
95
+    def get_list_view(self):
96
+        class ListViewClass(FilterView, ListView):
97
+            model = self.model
98
+            template_name = self.list_template_name
99
+            paginate_by = self.paginate_by
100
+            filterset_class = self.filterset_class
101
+            ordering = self.ordering
102
+
103
+            def get_context_data(inner_self, **kwargs):
104
+                context = super().get_context_data(**kwargs)
105
+                fields = self.get_fields()
106
+                context.update({
107
+                    'fields': [f for f in fields],
108
+
109
+                    # 'fields': [field for field in self.model._meta.get_fields()],
110
+                    'page_title': self.page_title,
111
+                    'list_url': self.list_url_name,
112
+                    'create_url': self.create_url_name,
113
+                    'update_url': self.update_url_name,
114
+                    'delete_url': self.delete_url_name,
115
+                    'bs': self.get_breadcrumbs('list'),
116
+                })
117
+                return context
118
+
119
+        return ListViewClass
120
+
121
+    def get_detail_view(self):
122
+        class DetailViewClass(DetailView):
123
+            model = self.model
124
+            template_name = self.detail_template_name
125
+
126
+            def get_context_data(inner_self, **kwargs):
127
+                context = super().get_context_data(**kwargs)
128
+                context.update({
129
+                    'page_title': self.page_title,
130
+                    'bs': self.get_breadcrumbs('detail'),
131
+                })
132
+                return context
133
+
134
+        return DetailViewClass
135
+
136
+    def get_create_view(self):
137
+        class CreateViewClass(CreateView):
138
+            model = self.model
139
+            template_name = self.form_template_name
140
+            fields = self.config_edit_fields
141
+
142
+            def form_valid(inner_self, form):
143
+                response = super().form_valid(form)
144
+                messages.success(inner_self.request, f"{self.model._meta.verbose_name} created successfully!")
145
+                return response
146
+
147
+            def get_success_url(inner_self):
148
+                return reverse(self.update_url_name, kwargs={'pk': inner_self.object.pk})
149
+
150
+            def get_context_data(inner_self, **kwargs):
151
+                context = super().get_context_data(**kwargs)
152
+                context.update({
153
+                    'list_url_name': self.list_url_name,
154
+                    'page_title': f"Create {self.model._meta.verbose_name}",
155
+                    'bs': self.get_breadcrumbs('update', obj=inner_self.object),
156
+
157
+                })
158
+                return context
159
+
160
+        return CreateViewClass
161
+
162
+    def get_update_view(self):
163
+        class UpdateViewClass(UpdateView):
164
+            model = self.model
165
+            template_name = self.form_template_name
166
+            fields = self.config_edit_fields
167
+
168
+            def form_valid(inner_self, form):
169
+                response = super().form_valid(form)
170
+                messages.success(inner_self.request, f"{self.model._meta.verbose_name} updated successfully!")
171
+                return response
172
+
173
+            def get_form(inner_self, *args, **kwargs):
174
+                """
175
+                Customize the form to make specified fields read-only.
176
+                """
177
+                form = super().get_form(*args, **kwargs)
178
+                for field_name in self.config_readonly_fields:
179
+                    if field_name in form.fields:
180
+                        form.fields[field_name].widget.attrs['readonly'] = True
181
+                        form.fields[field_name].widget.attrs['style'] = 'background-color: #f9f9f9; cursor: not-allowed;'
182
+                        form.fields[field_name].disabled = True  # Optional: Completely disable the field
183
+                return form
184
+
185
+            def get_success_url(inner_self):
186
+                return reverse(self.update_url_name, kwargs={'pk': inner_self.object.pk})
187
+
188
+            def get_context_data(inner_self, **kwargs):
189
+                context = super().get_context_data(**kwargs)
190
+                context.update({
191
+                    'list_url_name': self.list_url_name,
192
+                    'page_title': f"Update {self.model._meta.verbose_name}",
193
+                    'bs': self.get_breadcrumbs('update', obj=inner_self.object),
194
+                })
195
+                return context
196
+
197
+        return UpdateViewClass
198
+
199
+    def get_delete_view(self):
200
+        class DeleteViewClass(DeleteView):
201
+            model = self.model
202
+            template_name = self.confirm_delete_template_name
203
+            success_url = reverse_lazy(self.delete_url_name)
204
+
205
+            def get_context_data(inner_self, **kwargs):
206
+                context = super().get_context_data(**kwargs)
207
+                context.update({
208
+                    'list_url_name': self.list_url_name,
209
+                    'page_title': f"Delete {self.model._meta.verbose_name}",
210
+                    'bs': self.get_breadcrumbs('delete', obj=inner_self.object),
211
+                })
212
+                return context
213
+
214
+        return DeleteViewClass

+ 1 - 1
app/legacy/templates/legacy/datacrud_form.html

@@ -24,7 +24,7 @@
24 24
     </h1>
25 25
 
26 26
     <!-- Render the Form -->
27
-    <form method="post" >
27
+    <form method="post" enctype="multipart/form-data" >
28 28
         {% csrf_token %}
29 29
         <div  class="flex flex-wrap items-center space-x-4">
30 30
           

+ 2 - 0
app/legacy/templates/legacy/datacrud_list.html

@@ -44,6 +44,8 @@
44 44
                         <a href="{% url update_url obj.pk %}" class="text-blue-500 hover:underline">
45 45
                             {{ obj|attr:field.name }}
46 46
                         </a>
47
+                    {% elif field.name == 'file' and obj.file %}
48
+                    <a href="{{ obj.file.url }}" target="_blank">View</a>
47 49
                     {% else %}
48 50
                         {{ obj|attr:field.name }}
49 51
                     {% endif %}

+ 2 - 200
app/legacy/views.py

@@ -17,6 +17,8 @@ from django.urls import reverse
17 17
 from django.contrib import messages
18 18
 from pprint import pprint
19 19
 
20
+from core.utils import ConfigurableCRUDView
21
+
20 22
 class DataListView(FilterView, ListView):
21 23
     model = Data
22 24
     template_name = 'legacy/data_list.html'
@@ -112,206 +114,6 @@ class DataDeleteView(DeleteView):
112 114
         ]
113 115
         return context
114 116
 
115
-class ConfigurableCRUDView:
116
-    model = None
117
-    list_template_name = None
118
-    detail_template_name = None
119
-    form_template_name = None
120
-    confirm_delete_template_name = None
121
-    fields = '__all__'  # Default to all fields
122
-    paginate_by = 10
123
-    filterset_class = None
124
-    page_title = "Page Title"
125
-    create_url_name = None
126
-    update_url_name = None
127
-    delete_url_name = None
128
-    list_url_name = None
129
-    config_fields = "all"  # "all" or a list of field names to display
130
-    config_field_orders = []  # Fields to display first
131
-    config_excludes = []  # Fields to exclude
132
-
133
-    config_readonly_fields = []  # Fields that should be read-only in update view
134
-
135
-    config_edit_fields = "__all__"  # "all" or a list of field names to display
136
-    ordering = None
137
-
138
-
139
-
140
-    def get_fields(self):
141
-        """
142
-        Dynamically generate the fields to be displayed in the list view.
143
-        - Includes all fields if `config_fields` is set to "all".
144
-        - Respects `config_field_orders` and `config_excludes`.
145
-        Returns field instances instead of field names.
146
-        """
147
-        model_fields = {f.name: f for f in self.model._meta.get_fields()}
148
-
149
-        # Filter based on `config_fields` configuration
150
-        fields = []
151
-        if self.config_fields != "all":
152
-            fields = [model_fields[f] for f in self.config_fields if f in model_fields]
153
-        else:
154
-            fields = list(model_fields.values())
155
-        # pprint("------------------------")
156
-        # pprint(f"fields = {fields}")
157
-
158
-        # Exclude fields specified in `config_excludes`
159
-        fields = [f for f in fields if f.name not in self.config_excludes]
160
-
161
-        # Order fields based on `config_field_orders`
162
-        ordered_fields = [f for f in fields if f.name in self.config_field_orders]
163
-        remaining_fields = [f for f in fields if f.name not in self.config_field_orders]
164
-
165
-        # Reorder fields to match the order specified in `config_field_orders`
166
-        ordered_field_names = set(self.config_field_orders)
167
-        ordered_fields.sort(key=lambda f: self.config_field_orders.index(f.name) if f.name in ordered_field_names else len(ordered_field_names))
168
-
169
-        return ordered_fields + remaining_fields
170
-
171
-
172
-    def get_breadcrumbs(self, view_type, obj=None):
173
-        """
174
-        Generate breadcrumbs dynamically based on the view type.
175
-        :param view_type: The type of view (list, create, update, detail, delete)
176
-        :param obj: The current object (if applicable)
177
-        :return: A list of breadcrumb dictionaries
178
-        """
179
-        breadcrumbs = [
180
-            {'label': 'Dashboard', 'url': reverse('dashboard:index')},
181
-            {'label': self.page_title, 'url': reverse(self.list_url_name)} if self.list_url_name else None,
182
-        ]
183
-
184
-        if view_type == 'create':
185
-            breadcrumbs.append({'label': f'Create {self.model._meta.verbose_name}', 'url': None})
186
-        elif view_type == 'update' and obj:
187
-            breadcrumbs.append({'label': obj, 'url': reverse(self.update_url_name, kwargs={'pk': obj.pk})})
188
-        elif view_type == 'detail' and obj:
189
-            breadcrumbs.append({'label': f'Detail of {obj}', 'url': None})
190
-        elif view_type == 'delete' and obj:
191
-            breadcrumbs.append({'label': f'Delete {obj}', 'url': None})
192
-
193
-        return [b for b in breadcrumbs if b]
194
-
195
-    def get_list_view(self):
196
-        class ListViewClass(FilterView, ListView):
197
-            model = self.model
198
-            template_name = self.list_template_name
199
-            paginate_by = self.paginate_by
200
-            filterset_class = self.filterset_class
201
-            ordering = self.ordering
202
-
203
-            def get_context_data(inner_self, **kwargs):
204
-                context = super().get_context_data(**kwargs)
205
-                fields = self.get_fields()
206
-                context.update({
207
-                    'fields': [f for f in fields],
208
-
209
-                    # 'fields': [field for field in self.model._meta.get_fields()],
210
-                    'page_title': self.page_title,
211
-                    'list_url': self.list_url_name,
212
-                    'create_url': self.create_url_name,
213
-                    'update_url': self.update_url_name,
214
-                    'delete_url': self.delete_url_name,
215
-                    'bs': self.get_breadcrumbs('list'),
216
-                })
217
-                return context
218
-
219
-        return ListViewClass
220
-
221
-    def get_detail_view(self):
222
-        class DetailViewClass(DetailView):
223
-            model = self.model
224
-            template_name = self.detail_template_name
225
-
226
-            def get_context_data(inner_self, **kwargs):
227
-                context = super().get_context_data(**kwargs)
228
-                context.update({
229
-                    'page_title': self.page_title,
230
-                    'bs': self.get_breadcrumbs('detail'),
231
-                })
232
-                return context
233
-
234
-        return DetailViewClass
235
-
236
-    def get_create_view(self):
237
-        class CreateViewClass(CreateView):
238
-            model = self.model
239
-            template_name = self.form_template_name
240
-            fields = self.config_edit_fields
241
-
242
-            def form_valid(inner_self, form):
243
-                response = super().form_valid(form)
244
-                messages.success(inner_self.request, f"{self.model._meta.verbose_name} created successfully!")
245
-                return response
246
-
247
-            def get_success_url(inner_self):
248
-                return reverse(self.update_url_name, kwargs={'pk': inner_self.object.pk})
249
-
250
-            def get_context_data(inner_self, **kwargs):
251
-                context = super().get_context_data(**kwargs)
252
-                context.update({
253
-                    'list_url_name': self.list_url_name,
254
-                    'page_title': f"Create {self.model._meta.verbose_name}",
255
-                    'bs': self.get_breadcrumbs('update', obj=inner_self.object),
256
-
257
-                })
258
-                return context
259
-
260
-        return CreateViewClass
261
-
262
-    def get_update_view(self):
263
-        class UpdateViewClass(UpdateView):
264
-            model = self.model
265
-            template_name = self.form_template_name
266
-            fields = self.config_edit_fields
267
-
268
-            def form_valid(inner_self, form):
269
-                response = super().form_valid(form)
270
-                messages.success(inner_self.request, f"{self.model._meta.verbose_name} updated successfully!")
271
-                return response
272
-
273
-            def get_form(inner_self, *args, **kwargs):
274
-                """
275
-                Customize the form to make specified fields read-only.
276
-                """
277
-                form = super().get_form(*args, **kwargs)
278
-                for field_name in self.config_readonly_fields:
279
-                    if field_name in form.fields:
280
-                        form.fields[field_name].widget.attrs['readonly'] = True
281
-                        form.fields[field_name].widget.attrs['style'] = 'background-color: #f9f9f9; cursor: not-allowed;'
282
-                        form.fields[field_name].disabled = True  # Optional: Completely disable the field
283
-                return form
284
-
285
-            def get_success_url(inner_self):
286
-                return reverse(self.update_url_name, kwargs={'pk': inner_self.object.pk})
287
-
288
-            def get_context_data(inner_self, **kwargs):
289
-                context = super().get_context_data(**kwargs)
290
-                context.update({
291
-                    'list_url_name': self.list_url_name,
292
-                    'page_title': f"Update {self.model._meta.verbose_name}",
293
-                    'bs': self.get_breadcrumbs('update', obj=inner_self.object),
294
-                })
295
-                return context
296
-
297
-        return UpdateViewClass
298
-
299
-    def get_delete_view(self):
300
-        class DeleteViewClass(DeleteView):
301
-            model = self.model
302
-            template_name = self.confirm_delete_template_name
303
-            success_url = reverse_lazy(self.delete_url_name)
304
-
305
-            def get_context_data(inner_self, **kwargs):
306
-                context = super().get_context_data(**kwargs)
307
-                context.update({
308
-                    'list_url_name': self.list_url_name,
309
-                    'page_title': f"Delete {self.model._meta.verbose_name}",
310
-                    'bs': self.get_breadcrumbs('delete', obj=inner_self.object),
311
-                })
312
-                return context
313
-
314
-        return DeleteViewClass
315 117
 
316 118
 class DataMsCRUDView(ConfigurableCRUDView):
317 119
     model = DataMs

+ 28 - 0
app/report/filters.py

@@ -0,0 +1,28 @@
1
+import django_filters
2
+from core.models import Report
3
+
4
+class ReportFilter(django_filters.FilterSet):
5
+    name = django_filters.CharFilter(
6
+        field_name='name', 
7
+        lookup_expr='icontains', 
8
+        label='Report Name'
9
+    )
10
+    created_by = django_filters.CharFilter(
11
+        field_name='created_by__username', 
12
+        lookup_expr='icontains', 
13
+        label='Created By'
14
+    )
15
+    created_at = django_filters.DateFromToRangeFilter(
16
+        field_name='created_at', 
17
+        label='Created Date Range',
18
+        widget=django_filters.widgets.RangeWidget(
19
+            attrs={
20
+                'type': 'date',
21
+                'class': 'w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-300'
22
+            }
23
+        )
24
+    )
25
+
26
+    class Meta:
27
+        model = Report
28
+        fields = ['name', 'created_by', 'created_at']

+ 26 - 0
app/report/templates/report/create.html

@@ -0,0 +1,26 @@
1
+{% extends 'base.html' %}
2
+{% load tailwind_filters %}
3
+
4
+{% block title %}Create Report{% endblock %}
5
+
6
+{% block content %}
7
+<div class="container mx-auto px-4 py-8">
8
+    <h1 class="text-2xl font-bold text-gray-800 mb-4">Create a New Report</h1>
9
+    
10
+    <!-- Form -->
11
+    <form method="post" class="bg-white p-6 rounded-lg shadow-md">
12
+        {% csrf_token %}
13
+        {{ form | crispy }} <!-- Using crispy filters if installed -->
14
+        <div class="mt-4">
15
+            <button type="submit" class="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 focus:outline-none focus:ring focus:ring-blue-300">
16
+                Create Report
17
+            </button>
18
+        </div>
19
+    </form>
20
+    
21
+    <!-- Back Link -->
22
+    <a href="{% url 'report:report_index' %}" class="text-sm text-blue-500 hover:underline mt-4 inline-block">
23
+        Back to Reports
24
+    </a>
25
+</div>
26
+{% endblock %}

+ 68 - 29
app/report/templates/report/index.html

@@ -1,38 +1,77 @@
1 1
 {% extends 'base.html' %}
2
-
2
+{% load tailwind_filters %}
3 3
 {% block title %}Report Dashboard{% endblock %}
4 4
 
5 5
 {% block content %}
6 6
 <div class="container mx-auto px-4 py-8">
7
-    <h1 class="text-3xl font-bold text-center mb-4">Welcome to the Report Dashboard</h1>
8
-    <p class="text-center text-gray-600">Below is the list of reports currently available:</p>
9
-    
10
-<form class="max-w-sm mx-auto">
11
-  <label for="countries" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Select an option</label>
12
-  <select id="countries" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
13
-    <option selected>Choose a country</option>
14
-    <option value="US">United States</option>
15
-    <option value="CA">Canada</option>
16
-    <option value="FR">France</option>
17
-    <option value="DE">Germany</option>
18
-  </select>
19
-</form>
20 7
 
8
+  <h1 class="text-2xl font-bold text-gray-800">Reports</h1>
9
+
10
+    <!-- Filter Form -->
11
+        <form method="get" class="flex items-center space-x-4 mb-4">
12
+            {{ filter.form | crispy }}
13
+            <button type="submit" class="bg-blue-500 text-white py-2 px-4 rounded-lg">
14
+                Apply Filters
15
+            </button>
16
+        </form>
17
+        <div class="flex justify-between items-center mb-6">
18
+          <a href="{% url 'report:report_create' %}" 
19
+             class="bg-green-500 text-white py-2 px-4 rounded-lg hover:bg-green-600 focus:outline-none focus:ring focus:ring-green-300">
20
+            + Create New Report
21
+          </a>
22
+        </div>
21 23
 
22
-    <!-- Reports List -->
23
-    <div class="mt-6">
24
-        {% if reports %}
25
-            <ul class="bg-white rounded shadow p-4">
26
-                {% for report in reports %}
27
-                    <li class="border-b last:border-none py-2">
28
-                        <strong>{{ report.name }}</strong>
29
-                        <span class="text-sm text-gray-500">- Created on {{ report.date_created }}</span>
30
-                    </li>
31
-                {% endfor %}
32
-            </ul>
33
-        {% else %}
34
-            <p class="text-gray-500 text-center">No reports available at the moment. Please check back later.</p>
35
-        {% endif %}
24
+          <div class="bg-white rounded-lg shadow-md p-5">
25
+            <table class="min-w-full table-auto border-collapse border border-gray-300">
26
+                <thead>
27
+                    <tr class="bg-gray-200">
28
+                        <th class="border px-4 py-2 text-left">#</th>
29
+                        <th class="border px-4 py-2 text-left">Name</th>
30
+                        <th class="border px-4 py-2 text-left">Created By</th>
31
+                        <th class="border px-4 py-2 text-left">Created At</th>
32
+                    </tr>
33
+                </thead>
34
+                <tbody>
35
+                    {% for report in page_obj %}
36
+                    <tr>
37
+                        <td class="border px-4 py-2">{{ forloop.counter }}</td>
38
+                        <td class="border px-4 py-2">{{ report.name }}</td>
39
+                        <td class="border px-4 py-2">{{ report.created_by }}</td>
40
+                        <td class="border px-4 py-2">{{ report.created_at|date:"Y-m-d H:i" }}</td>
41
+                    </tr>
42
+                    {% empty %}
43
+                    <tr>
44
+                        <td colspan="4" class="border px-4 py-2 text-center text-gray-500">
45
+                            No reports found.
46
+                        </td>
47
+                    </tr>
48
+                    {% endfor %}
49
+                </tbody>
50
+            </table>
51
+        </div>
52
+    <div class="mt-6 flex justify-between items-center">
53
+        <div>
54
+            <span class="text-sm text-gray-600">
55
+                Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
56
+            </span>
57
+        </div>
58
+        <div class="space-x-2">
59
+            {% if page_obj.has_previous %}
60
+                <a href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value|urlencode }}&{% endif %}{% endfor %}page=1" 
61
+                   class="text-blue-500 hover:underline">First</a>
62
+                <a href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value|urlencode }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}" 
63
+                   class="text-blue-500 hover:underline">Previous</a>
64
+            {% endif %}
65
+            {% if page_obj.has_next %}
66
+                <a href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value|urlencode }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}" 
67
+                   class="text-blue-500 hover:underline">Next</a>
68
+                <a href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value|urlencode }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}" 
69
+                   class="text-blue-500 hover:underline">Last</a>
70
+            {% endif %}
71
+        </div>
36 72
     </div>
73
+
74
+
75
+  <!-- Data Grid -->
37 76
 </div>
38
-{% endblock %}
77
+  {% endblock %}

+ 9 - 1
app/report/urls.py

@@ -1,10 +1,18 @@
1 1
 from django.urls import path
2 2
 from . import views
3
+from .views import ReportCRUDView
4
+
5
+report_crud = ReportCRUDView()
3 6
 
4 7
 app_name = "report"  # Use this namespace for reverse URL lookups
5 8
 
6 9
 urlpatterns = [
7
-    path('', views.index, name='index'),  # Homepage for the report app
10
+    # path('reports/', views.index, name='report_index'),  # Homepage for the report app
11
+    # path('reports/create/', views.report_create_view, name='report_create'),
12
+    path('report/', report_crud.get_list_view().as_view(), name='report-list'),
13
+    path('report/create/', report_crud.get_create_view().as_view(), name='report-create'),
14
+    path('report/<int:pk>/update/', report_crud.get_update_view().as_view(), name='report-update'),
15
+    path('report/<int:pk>/delete/', report_crud.get_delete_view().as_view(), name='report-delete'),
8 16
     # path('create/', views.create_report, name='create'),  # Create a new report
9 17
     # path('<int:pk>/', views.detail_report, name='detail'),  # View details of a specific report
10 18
     # path('<int:pk>/update/', views.update_report, name='update'),  # Update a specific report

+ 52 - 7
app/report/views.py

@@ -1,13 +1,58 @@
1
-from django.shortcuts import render
1
+from django.shortcuts import render, redirect, get_object_or_404
2
+from django.core.paginator import Paginator
3
+from django.contrib import messages
4
+from core.models import Report
5
+from core.forms import ReportForm
6
+from core.utils import ConfigurableCRUDView
7
+from .filters import ReportFilter
8
+
2 9
 
3 10
 
4 11
 def index(request):
5
-    """
6
-    Renders the index page for the report app.
7
-    """
8
-    # Optional: Pass any context variables to the template
12
+    reports = Report.objects.all()
13
+    report_filter = ReportFilter(request.GET, queryset=reports)
14
+    
15
+    # Paginate the filtered queryset
16
+    paginator = Paginator(report_filter.qs, 10)  # Show 10 reports per page
17
+    page_number = request.GET.get('page')
18
+    page_obj = paginator.get_page(page_number)
19
+    
9 20
     context = {
10
-        'page_title': 'Report Dashboard',
11
-        'reports': [],  # Replace with actual data, e.g., a queryset from the database
21
+        'filter': report_filter,
22
+        'page_obj': page_obj,
12 23
     }
13 24
     return render(request, 'report/index.html', context)
25
+
26
+def report_create_view(request):
27
+    if request.method == "POST":
28
+        form = ReportForm(request.POST)
29
+        if form.is_valid():
30
+            form.save()
31
+            messages.success(request, "Report created successfully!")
32
+            return redirect("report:report_index")  # Adjust with your report list view name
33
+    else:
34
+        form = ReportForm()
35
+
36
+    return render(request, "report/create.html", {"form": form})
37
+
38
+
39
+class ReportCRUDView(ConfigurableCRUDView):
40
+    model = Report
41
+    list_template_name = 'legacy/datacrud_list.html'
42
+    detail_template_name = 'legacy/datacrud_detail.html'
43
+    form_template_name = 'legacy/datacrud_form.html'
44
+    confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
45
+    filterset_class = ReportFilter
46
+
47
+    page_title = "Reports"
48
+
49
+    # URL name mappings
50
+    list_url_name = 'report:report-list'
51
+    create_url_name = 'report:report-create'
52
+    update_url_name = 'report:report-update'
53
+    delete_url_name = 'report:report-delete'
54
+    config_fields = ["name", "file", "created_by",] 
55
+    config_field_orders = ["id", "name", "created_by", "created_at"]
56
+    # config_readonly_fields = ["lot_no"]
57
+    # config_edit_fields = ["lot_no", "code"]
58
+    ordering = ["-created_at", "-id",]

+ 2 - 2
app/templates/base.html

@@ -20,7 +20,7 @@
20 20
 </head>
21 21
 <body class="bg-gray-100 text-gray-800">
22 22
        <!-- Navbar -->
23
-    <nav class="fixed top-0 z-50 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
23
+    <nav class="fixed top-0 z-50 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700"  >
24 24
         <div class="px-3 py-3 lg:px-5 lg:pl-3">
25 25
             <div class="flex items-center justify-between">
26 26
                 <div class="flex items-center justify-start">
@@ -63,7 +63,7 @@
63 63
         <div class="h-full px-3 pb-4 overflow-y-auto">
64 64
             <ul class="space-y-2">
65 65
                 <li><a href="/dashboard/" 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">Dashboard</span></a></li>
66
-                <li><a href="{% url "report:index" %}" 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">Reports</span></a></li>
66
+                <li><a href="{% url "report:report-list" %}" 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">Reports</span></a></li>
67 67
                  <li>
68 68
                     <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="dropdown-example" data-collapse-toggle="dropdown-example">
69 69
                           <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">