TheHoodedFoot commited on
Commit
06b4e39
·
verified ·
1 Parent(s): d087b01

Add 3 files

Browse files
Files changed (3) hide show
  1. README.md +6 -4
  2. index.html +1241 -19
  3. prompts.txt +0 -0
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Psql Nodejs Dashboard
3
- emoji: 🌍
4
- colorFrom: red
5
  colorTo: yellow
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: psql-nodejs-dashboard
3
+ emoji: 🐳
4
+ colorFrom: green
5
  colorTo: yellow
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,1241 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Company Job Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.css">
11
+ <style>
12
+ .sortable:hover {
13
+ background-color: #f3f4f6;
14
+ cursor: pointer;
15
+ }
16
+ .sort-asc::after {
17
+ content: " ↑";
18
+ }
19
+ .sort-desc::after {
20
+ content: " ↓";
21
+ }
22
+ .modal {
23
+ transition: opacity 0.3s ease;
24
+ }
25
+ .chart-container {
26
+ position: relative;
27
+ height: 300px;
28
+ }
29
+ .invalid-row {
30
+ background-color: #fee2e2;
31
+ }
32
+ .edit-icon:hover {
33
+ color: #3b82f6;
34
+ transform: scale(1.1);
35
+ }
36
+ .timeframe-btn.active {
37
+ background-color: #3b82f6;
38
+ color: white;
39
+ }
40
+ .daterangepicker {
41
+ font-family: inherit;
42
+ }
43
+ #error-alert {
44
+ transition: all 0.3s ease;
45
+ }
46
+ </style>
47
+ </head>
48
+ <body class="bg-gray-50">
49
+ <!-- Error Alert -->
50
+ <div id="error-alert" class="fixed top-4 right-4 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg hidden z-50">
51
+ <div class="flex items-center">
52
+ <i class="fas fa-exclamation-circle mr-3"></i>
53
+ <span id="error-message">Database connection error</span>
54
+ <button id="close-error" class="ml-4">
55
+ <i class="fas fa-times"></i>
56
+ </button>
57
+ </div>
58
+ </div>
59
+
60
+ <!-- Loading Overlay -->
61
+ <div id="loading-overlay" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-40">
62
+ <div class="bg-white p-8 rounded-lg shadow-xl text-center">
63
+ <div class="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-500 mx-auto mb-4"></div>
64
+ <p class="text-lg font-semibold">Loading dashboard data...</p>
65
+ </div>
66
+ </div>
67
+
68
+ <div class="container mx-auto px-4 py-8">
69
+ <!-- Header with Add Job button -->
70
+ <div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
71
+ <div>
72
+ <h1 class="text-3xl font-bold text-gray-800">Job Dashboard</h1>
73
+ <p class="text-gray-600">Weekly job completion overview</p>
74
+ </div>
75
+
76
+ <div class="flex items-center gap-4">
77
+ <!-- Search -->
78
+ <div class="relative w-full md:w-64">
79
+ <input type="text" id="search" placeholder="Search jobs..." class="w-full pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
80
+ <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
81
+ </div>
82
+
83
+ <!-- Add Job Button -->
84
+ <button id="add-job-btn" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg flex items-center">
85
+ <i class="fas fa-plus mr-2"></i> Add Job
86
+ </button>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Stats Cards -->
91
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8" id="stats-cards">
92
+ <!-- Will be populated by JavaScript -->
93
+ </div>
94
+
95
+ <!-- Charts Row -->
96
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
97
+ <div class="bg-white p-6 rounded-xl shadow">
98
+ <div class="flex justify-between items-center mb-4">
99
+ <h3 class="text-lg font-semibold text-gray-800">Revenue Trend</h3>
100
+ <div class="flex space-x-1">
101
+ <button class="timeframe-btn active px-3 py-1 rounded-md text-sm" data-timeframe="week">Week</button>
102
+ <button class="timeframe-btn px-3 py-1 rounded-md text-sm" data-timeframe="month">Month</button>
103
+ <button class="timeframe-btn px-3 py-1 rounded-md text-sm" data-timeframe="quarter">Quarter</button>
104
+ <button class="timeframe-btn px-3 py-1 rounded-md text-sm" data-timeframe="year">Year</button>
105
+ </div>
106
+ </div>
107
+ <div class="chart-container">
108
+ <canvas id="trendChart"></canvas>
109
+ </div>
110
+ </div>
111
+
112
+ <div class="bg-white p-6 rounded-xl shadow">
113
+ <h3 class="text-lg font-semibold text-gray-800 mb-4">Jobs by Customer</h3>
114
+ <div class="chart-container">
115
+ <canvas id="pieChart"></canvas>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Jobs Table -->
121
+ <div class="bg-white rounded-xl shadow overflow-hidden mb-8">
122
+ <div class="px-6 py-4 border-b flex justify-between items-center">
123
+ <h2 class="text-xl font-semibold text-gray-800">Completed Jobs - <span id="date-range-text">Week of May 15, 2023</span></h2>
124
+ <div class="flex items-center gap-2">
125
+ <input type="text" id="date-range-picker" class="border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer" placeholder="Select date range">
126
+ <button id="this-week-btn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg">This Week</button>
127
+ </div>
128
+ </div>
129
+ <div class="overflow-x-auto">
130
+ <table class="min-w-full divide-y divide-gray-200">
131
+ <thead class="bg-gray-50">
132
+ <tr>
133
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sortable sort-asc" data-column="customer">
134
+ Customer
135
+ </th>
136
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sortable" data-column="jobId">
137
+ Job ID
138
+ </th>
139
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
140
+ Description
141
+ </th>
142
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sortable" data-column="quantity">
143
+ Qty
144
+ </th>
145
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sortable" data-column="price">
146
+ Price
147
+ </th>
148
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sortable" data-column="total">
149
+ Total
150
+ </th>
151
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
152
+ Thumbnail
153
+ </th>
154
+ <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
155
+ Actions
156
+ </th>
157
+ </tr>
158
+ </thead>
159
+ <tbody class="bg-white divide-y divide-gray-200" id="jobs-table-body">
160
+ <!-- Jobs will be inserted here by JavaScript -->
161
+ </tbody>
162
+ </table>
163
+ </div>
164
+ </div>
165
+
166
+ <!-- Invalid Jobs Table -->
167
+ <div class="bg-white rounded-xl shadow overflow-hidden mb-8">
168
+ <div class="px-6 py-4 border-b bg-red-50">
169
+ <h2 class="text-xl font-semibold text-gray-800">Invalid Jobs - Missing Information</h2>
170
+ <p class="text-sm text-red-600">These jobs require additional details to be processed</p>
171
+ </div>
172
+ <div class="overflow-x-auto">
173
+ <table class="min-w-full divide-y divide-gray-200">
174
+ <thead class="bg-gray-50">
175
+ <tr>
176
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sortable" data-column="customer">
177
+ Customer
178
+ </th>
179
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sortable" data-column="jobId">
180
+ Job ID
181
+ </th>
182
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
183
+ Description
184
+ </th>
185
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
186
+ Missing Info
187
+ </th>
188
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
189
+ Thumbnail
190
+ </th>
191
+ <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
192
+ Actions
193
+ </th>
194
+ </tr>
195
+ </thead>
196
+ <tbody class="bg-white divide-y divide-gray-200" id="invalid-jobs-table-body">
197
+ <!-- Invalid jobs will be inserted here by JavaScript -->
198
+ </tbody>
199
+ </table>
200
+ </div>
201
+ </div>
202
+ </div>
203
+
204
+ <!-- Image Modal -->
205
+ <div id="image-modal" class="modal fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 opacity-0 pointer-events-none">
206
+ <div class="bg-white rounded-lg max-w-4xl w-full max-h-screen overflow-auto">
207
+ <div class="flex justify-between items-center p-4 border-b">
208
+ <h3 class="text-lg font-semibold">Job Image</h3>
209
+ <button id="close-modal" class="text-gray-500 hover:text-gray-700">
210
+ <i class="fas fa-times"></i>
211
+ </button>
212
+ </div>
213
+ <div class="p-4">
214
+ <img id="modal-image" src="" alt="Job Image" class="w-full h-auto rounded">
215
+ </div>
216
+ <div class="p-4 border-t text-right">
217
+ <button id="download-image" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
218
+ <i class="fas fa-download mr-2"></i>Download
219
+ </button>
220
+ </div>
221
+ </div>
222
+ </div>
223
+
224
+ <!-- Edit Job Modal -->
225
+ <div id="edit-modal" class="modal fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 opacity-0 pointer-events-none">
226
+ <div class="bg-white rounded-lg max-w-2xl w-full max-h-screen overflow-auto">
227
+ <div class="flex justify-between items-center p-4 border-b">
228
+ <h3 class="text-lg font-semibold">Edit Job</h3>
229
+ <button id="close-edit-modal" class="text-gray-500 hover:text-gray-700">
230
+ <i class="fas fa-times"></i>
231
+ </button>
232
+ </div>
233
+ <div class="p-4">
234
+ <form id="edit-job-form">
235
+ <input type="hidden" id="edit-job-id">
236
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
237
+ <div>
238
+ <label for="edit-customer" class="block text-sm font-medium text-gray-700 mb-1">Customer</label>
239
+ <input type="text" id="edit-customer" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
240
+ </div>
241
+ <div>
242
+ <label for="edit-job-id-display" class="block text-sm font-medium text-gray-700 mb-1">Job ID</label>
243
+ <input type="text" id="edit-job-id-display" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" disabled>
244
+ </div>
245
+ </div>
246
+ <div class="mb-4">
247
+ <label for="edit-description" class="block text-sm font-medium text-gray-700 mb-1">Description</label>
248
+ <textarea id="edit-description" rows="3" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
249
+ </div>
250
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
251
+ <div>
252
+ <label for="edit-quantity" class="block text-sm font-medium text-gray-700 mb-1">Quantity</label>
253
+ <input type="number" id="edit-quantity" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
254
+ </div>
255
+ <div>
256
+ <label for="edit-price" class="block text-sm font-medium text-gray-700 mb-1">Price</label>
257
+ <input type="number" step="0.01" id="edit-price" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
258
+ </div>
259
+ <div>
260
+ <label for="edit-total" class="block text-sm font-medium text-gray-700 mb-1">Total</label>
261
+ <input type="number" step="0.01" id="edit-total" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" disabled>
262
+ </div>
263
+ </div>
264
+ <div class="mb-4">
265
+ <label class="block text-sm font-medium text-gray-700 mb-1">Thumbnail</label>
266
+ <div class="flex items-center gap-4">
267
+ <img id="edit-thumbnail" src="" alt="Thumbnail" class="w-20 h-20 object-cover rounded border">
268
+ <button type="button" id="change-image" class="bg-gray-100 hover:bg-gray-200 text-gray-800 px-4 py-2 rounded-lg">
269
+ <i class="fas fa-image mr-2"></i>Change Image
270
+ </button>
271
+ </div>
272
+ </div>
273
+ </form>
274
+ </div>
275
+ <div class="p-4 border-t flex justify-between">
276
+ <button id="delete-job" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded">
277
+ <i class="fas fa-trash mr-2"></i>Delete Job
278
+ </button>
279
+ <div>
280
+ <button id="cancel-edit" class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded mr-2">
281
+ Cancel
282
+ </button>
283
+ <button id="save-job" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
284
+ <i class="fas fa-save mr-2"></i>Save Changes
285
+ </button>
286
+ </div>
287
+ </div>
288
+ </div>
289
+ </div>
290
+
291
+ <!-- Add Job Modal -->
292
+ <div id="add-modal" class="modal fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 opacity-0 pointer-events-none">
293
+ <div class="bg-white rounded-lg max-w-2xl w-full max-h-screen overflow-auto">
294
+ <div class="flex justify-between items-center p-4 border-b">
295
+ <h3 class="text-lg font-semibold">Add New Job</h3>
296
+ <button id="close-add-modal" class="text-gray-500 hover:text-gray-700">
297
+ <i class="fas fa-times"></i>
298
+ </button>
299
+ </div>
300
+ <div class="p-4">
301
+ <form id="add-job-form">
302
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
303
+ <div>
304
+ <label for="add-customer" class="block text-sm font-medium text-gray-700 mb-1">Customer</label>
305
+ <input type="text" id="add-customer" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required>
306
+ </div>
307
+ <div>
308
+ <label for="add-job-id" class="block text-sm font-medium text-gray-700 mb-1">Job ID</label>
309
+ <input type="text" id="add-job-id" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required>
310
+ </div>
311
+ </div>
312
+ <div class="mb-4">
313
+ <label for="add-description" class="block text-sm font-medium text-gray-700 mb-1">Description</label>
314
+ <textarea id="add-description" rows="3" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required></textarea>
315
+ </div>
316
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
317
+ <div>
318
+ <label for="add-quantity" class="block text-sm font-medium text-gray-700 mb-1">Quantity</label>
319
+ <input type="number" id="add-quantity" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required>
320
+ </div>
321
+ <div>
322
+ <label for="add-price" class="block text-sm font-medium text-gray-700 mb-1">Price</label>
323
+ <input type="number" step="0.01" id="add-price" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required>
324
+ </div>
325
+ <div>
326
+ <label for="add-total" class="block text-sm font-medium text-gray-700 mb-1">Total</label>
327
+ <input type="number" step="0.01" id="add-total" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" disabled>
328
+ </div>
329
+ </div>
330
+ <div class="mb-4">
331
+ <label class="block text-sm font-medium text-gray-700 mb-1">Thumbnail</label>
332
+ <div class="flex items-center gap-4">
333
+ <img id="add-thumbnail" src="https://via.placeholder.com/100" alt="Thumbnail" class="w-20 h-20 object-cover rounded border">
334
+ <button type="button" id="add-change-image" class="bg-gray-100 hover:bg-gray-200 text-gray-800 px-4 py-2 rounded-lg">
335
+ <i class="fas fa-image mr-2"></i>Upload Image
336
+ </button>
337
+ </div>
338
+ </div>
339
+ </form>
340
+ </div>
341
+ <div class="p-4 border-t flex justify-end">
342
+ <button id="cancel-add" class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded mr-2">
343
+ Cancel
344
+ </button>
345
+ <button id="save-new-job" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded">
346
+ <i class="fas fa-plus mr-2"></i>Add Job
347
+ </button>
348
+ </div>
349
+ </div>
350
+ </div>
351
+
352
+ <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
353
+ <script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.min.js"></script>
354
+ <script src="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.min.js"></script>
355
+ <script>
356
+ // Global variables for data
357
+ let jobsData = [];
358
+ let invalidJobsData = [];
359
+ let weeklyTrendData = [];
360
+ let monthlyTrendData = [];
361
+ let quarterlyTrendData = [];
362
+ let yearlyTrendData = [];
363
+ let statsData = {};
364
+
365
+ // Base URL for Node.js API endpoints
366
+ const API_BASE_URL = 'http://localhost:3000/api';
367
+
368
+ // Initialize the dashboard
369
+ document.addEventListener('DOMContentLoaded', async function() {
370
+ // Show loading overlay
371
+ document.getElementById('loading-overlay').classList.remove('hidden');
372
+
373
+ try {
374
+ // Fetch all data from Node.js API
375
+ await fetchAllData();
376
+
377
+ // Initialize date range picker
378
+ initDateRangePicker();
379
+
380
+ // Set current week as default
381
+ setCurrentWeek();
382
+
383
+ // Render jobs tables
384
+ renderJobsTable();
385
+ renderInvalidJobsTable();
386
+
387
+ // Render stats cards
388
+ renderStatsCards();
389
+
390
+ // Initialize charts
391
+ initTrendChart('week');
392
+ initPieChart();
393
+
394
+ // Set up event listeners
395
+ setupEventListeners();
396
+
397
+ // Hide loading overlay
398
+ document.getElementById('loading-overlay').classList.add('hidden');
399
+ } catch (error) {
400
+ console.error('Error initializing dashboard:', error);
401
+ document.getElementById('loading-overlay').classList.add('hidden');
402
+ showError('Failed to load dashboard data. Please try again later.');
403
+ }
404
+ });
405
+
406
+ /*
407
+ Required Node.js API Endpoints (using Express and node-postgres):
408
+
409
+ 1. GET /api/jobs?status=completed&startDate=&endDate=
410
+ - Returns completed jobs within date range (if provided)
411
+ - SQL: SELECT * FROM jobs WHERE status = $1 AND ($2 IS NULL OR completion_date >= $2)
412
+ AND ($3 IS NULL OR completion_date <= $3) ORDER BY completion_date DESC
413
+ - Parameters: [status, startDate, endDate]
414
+
415
+ 2. GET /api/jobs?status=invalid
416
+ - Returns invalid jobs
417
+ - SQL: SELECT * FROM jobs WHERE status = $1 ORDER BY created_at DESC
418
+ - Parameters: [status]
419
+
420
+ 3. GET /api/stats/trend?timeframe=week
421
+ - Returns trend data for given timeframe (week/month/quarter/year)
422
+ - SQL varies based on timeframe (see below for examples)
423
+
424
+ 4. GET /api/stats/summary
425
+ - Returns summary statistics
426
+ - SQL:
427
+ SELECT
428
+ SUM(CASE WHEN completion_date BETWEEN CURRENT_DATE - INTERVAL '7 days' AND CURRENT_DATE THEN quantity * price ELSE 0 END) AS week_total,
429
+ ...other stats...
430
+ FROM jobs
431
+ WHERE status = 'completed'
432
+
433
+ 5. POST /api/jobs
434
+ - Creates a new job
435
+ - SQL: INSERT INTO jobs (job_id, customer_name, description, quantity, price, status, completion_date)
436
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
437
+ - Parameters: [jobId, customer, description, quantity, price, 'completed', new Date()]
438
+
439
+ 6. PUT /api/jobs/:id
440
+ - Updates a job
441
+ - SQL: UPDATE jobs SET customer_name = $1, description = $2, quantity = $3, price = $4,
442
+ updated_at = NOW() WHERE job_id = $5
443
+ - Parameters: [customer, description, quantity, price, jobId]
444
+
445
+ 7. DELETE /api/jobs/:id
446
+ - Deletes a job
447
+ - SQL: DELETE FROM jobs WHERE job_id = $1
448
+ - Parameters: [jobId]
449
+ */
450
+
451
+ // Fetch all data from Node.js API
452
+ async function fetchAllData() {
453
+ try {
454
+ // Fetch jobs data
455
+ const jobsResponse = await fetch(`${API_BASE_URL}/jobs?status=completed`);
456
+ if (!jobsResponse.ok) throw new Error('Failed to fetch jobs');
457
+ jobsData = await jobsResponse.json();
458
+
459
+ // Fetch invalid jobs data
460
+ const invalidJobsResponse = await fetch(`${API_BASE_URL}/jobs?status=invalid`);
461
+ if (!invalidJobsResponse.ok) throw new Error('Failed to fetch invalid jobs');
462
+ invalidJobsData = await invalidJobsResponse.json();
463
+
464
+ // Fetch trend data
465
+ const weeklyTrendResponse = await fetch(`${API_BASE_URL}/stats/trend?timeframe=week`);
466
+ if (!weeklyTrendResponse.ok) throw new Error('Failed to fetch weekly trend');
467
+ weeklyTrendData = await weeklyTrendResponse.json();
468
+
469
+ const monthlyTrendResponse = await fetch(`${API_BASE_URL}/stats/trend?timeframe=month`);
470
+ if (!monthlyTrendResponse.ok) throw new Error('Failed to fetch monthly trend');
471
+ monthlyTrendData = await monthlyTrendResponse.json();
472
+
473
+ const quarterlyTrendResponse = await fetch(`${API_BASE_URL}/stats/trend?timeframe=quarter`);
474
+ if (!quarterlyTrendResponse.ok) throw new Error('Failed to fetch quarterly trend');
475
+ quarterlyTrendData = await quarterlyTrendResponse.json();
476
+
477
+ const yearlyTrendResponse = await fetch(`${API_BASE_URL}/stats/trend?timeframe=year`);
478
+ if (!yearlyTrendResponse.ok) throw new Error('Failed to fetch yearly trend');
479
+ yearlyTrendData = await yearlyTrendResponse.json();
480
+
481
+ // Fetch stats data
482
+ const statsResponse = await fetch(`${API_BASE_URL}/stats/summary`);
483
+ if (!statsResponse.ok) throw new Error('Failed to fetch stats');
484
+ const stats = await statsResponse.json();
485
+ statsData = stats[0] || {};
486
+
487
+ } catch (error) {
488
+ console.error('Error fetching data:', error);
489
+ throw error;
490
+ }
491
+ }
492
+
493
+ // Render stats cards
494
+ function renderStatsCards() {
495
+ const statsContainer = document.getElementById('stats-cards');
496
+
497
+ statsContainer.innerHTML = `
498
+ <div class="bg-white p-6 rounded-xl shadow">
499
+ <div class="flex justify-between items-start">
500
+ <div>
501
+ <p class="text-gray-500">This Week's Total</p>
502
+ <p class="text-3xl font-bold text-gray-800">£${(statsData.week_total || 0).toLocaleString('en-GB', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</p>
503
+ </div>
504
+ <div class="flex items-center">
505
+ <span class="${statsData.week_change >= 0 ? 'text-green-500' : 'text-red-500'} font-bold">${statsData.week_change >= 0 ? '+' : ''}${(statsData.week_change || 0).toFixed(1)}%</span>
506
+ <i class="fas ${statsData.week_change >= 0 ? 'fa-arrow-up text-green-500' : 'fa-arrow-down text-red-500'} ml-1"></i>
507
+ </div>
508
+ </div>
509
+ <p class="text-sm text-gray-500 mt-2">vs last week</p>
510
+ </div>
511
+
512
+ <div class="bg-white p-6 rounded-xl shadow">
513
+ <div class="flex justify-between items-start">
514
+ <div>
515
+ <p class="text-gray-500">Jobs Completed</p>
516
+ <p class="text-3xl font-bold text-gray-800">${statsData.jobs_completed || 0}</p>
517
+ </div>
518
+ <div class="flex items-center">
519
+ <span class="${statsData.jobs_change >= 0 ? 'text-green-500' : 'text-red-500'} font-bold">${statsData.jobs_change >= 0 ? '+' : ''}${(statsData.jobs_change || 0).toFixed(1)}%</span>
520
+ <i class="fas ${statsData.jobs_change >= 0 ? 'fa-arrow-up text-green-500' : 'fa-arrow-down text-red-500'} ml-1"></i>
521
+ </div>
522
+ </div>
523
+ <p class="text-sm text-gray-500 mt-2">vs last week</p>
524
+ </div>
525
+
526
+ <div class="bg-white p-6 rounded-xl shadow">
527
+ <div class="flex justify-between items-start">
528
+ <div>
529
+ <p class="text-gray-500">Avg. Job Value</p>
530
+ <p class="text-3xl font-bold text-gray-800">£${(statsData.avg_job_value || 0).toLocaleString('en-GB', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</p>
531
+ </div>
532
+ <div class="flex items-center">
533
+ <span class="${statsData.avg_value_change >= 0 ? 'text-green-500' : 'text-red-500'} font-bold">${statsData.avg_value_change >= 0 ? '+' : ''}${(statsData.avg_value_change || 0).toFixed(1)}%</span>
534
+ <i class="fas ${statsData.avg_value_change >= 0 ? 'fa-arrow-up text-green-500' : 'fa-arrow-down text-red-500'} ml-1"></i>
535
+ </div>
536
+ </div>
537
+ <p class="text-sm text-gray-500 mt-2">vs last week</p>
538
+ </div>
539
+ `;
540
+ }
541
+
542
+ // Initialize date range picker
543
+ function initDateRangePicker() {
544
+ $('#date-range-picker').daterangepicker({
545
+ opens: 'left',
546
+ autoUpdateInput: false,
547
+ locale: {
548
+ cancelLabel: 'Clear',
549
+ format: 'DD MMM YYYY',
550
+ applyLabel: 'Apply',
551
+ cancelLabel: 'Cancel',
552
+ fromLabel: 'From',
553
+ toLabel: 'To',
554
+ customRangeLabel: 'Custom',
555
+ daysOfWeek: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
556
+ monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
557
+ firstDay: 1 // Monday
558
+ }
559
+ });
560
+
561
+ $('#date-range-picker').on('apply.daterangepicker', async function(ev, picker) {
562
+ $(this).val(picker.startDate.format('DD MMM YYYY') + ' - ' + picker.endDate.format('DD MMM YYYY'));
563
+ updateDateRangeText(picker.startDate, picker.endDate);
564
+
565
+ try {
566
+ // Show loading overlay
567
+ document.getElementById('loading-overlay').classList.remove('hidden');
568
+
569
+ // Fetch jobs for selected date range
570
+ const response = await fetch(`${API_BASE_URL}/jobs?status=completed&startDate=${picker.startDate.format('YYYY-MM-DD')}&endDate=${picker.endDate.format('YYYY-MM-DD')}`);
571
+ if (!response.ok) throw new Error('Failed to fetch jobs');
572
+
573
+ jobsData = await response.json();
574
+ renderJobsTable();
575
+
576
+ // Hide loading overlay
577
+ document.getElementById('loading-overlay').classList.add('hidden');
578
+ } catch (error) {
579
+ console.error('Error fetching jobs:', error);
580
+ document.getElementById('loading-overlay').classList.add('hidden');
581
+ showError('Failed to load jobs for selected date range.');
582
+ }
583
+ });
584
+
585
+ $('#date-range-picker').on('cancel.daterangepicker', function(ev, picker) {
586
+ $(this).val('');
587
+ showError('Date range selection cleared');
588
+ });
589
+ }
590
+
591
+ // Set current week (Monday to Friday)
592
+ function setCurrentWeek() {
593
+ const today = new Date();
594
+ const dayOfWeek = today.getDay(); // 0 (Sunday) to 6 (Saturday)
595
+
596
+ // Calculate Monday of current week
597
+ const monday = new Date(today);
598
+ monday.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1));
599
+
600
+ // Calculate Friday of current week
601
+ const friday = new Date(monday);
602
+ friday.setDate(monday.getDate() + 4);
603
+
604
+ // Format dates for display
605
+ const formattedMonday = moment(monday).format('DD MMM YYYY');
606
+ const formattedFriday = moment(friday).format('DD MMM YYYY');
607
+
608
+ // Update date range picker
609
+ $('#date-range-picker').val(formattedMonday + ' - ' + formattedFriday);
610
+
611
+ // Update date range text
612
+ updateDateRangeText(monday, friday);
613
+ }
614
+
615
+ // Update the date range text above the table
616
+ function updateDateRangeText(startDate, endDate) {
617
+ const start = moment(startDate);
618
+ const end = moment(endDate);
619
+
620
+ if (start.isSame(end, 'day')) {
621
+ document.getElementById('date-range-text').textContent = start.format('DD MMM YYYY');
622
+ } else if (start.isSame(end, 'month')) {
623
+ document.getElementById('date-range-text').textContent =
624
+ `${start.format('DD')}-${end.format('DD MMM YYYY')}`;
625
+ } else if (start.isSame(end, 'year')) {
626
+ document.getElementById('date-range-text').textContent =
627
+ `${start.format('DD MMM')}-${end.format('DD MMM YYYY')}`;
628
+ } else {
629
+ document.getElementById('date-range-text').textContent =
630
+ `${start.format('DD MMM YYYY')}-${end.format('DD MMM YYYY')}`;
631
+ }
632
+ }
633
+
634
+ // Render the main jobs table
635
+ function renderJobsTable() {
636
+ const tableBody = document.getElementById('jobs-table-body');
637
+ tableBody.innerHTML = '';
638
+
639
+ if (jobsData.length === 0) {
640
+ const row = document.createElement('tr');
641
+ row.innerHTML = `
642
+ <td colspan="8" class="px-6 py-4 text-center text-gray-500">
643
+ No jobs found for the selected date range
644
+ </td>
645
+ `;
646
+ tableBody.appendChild(row);
647
+ return;
648
+ }
649
+
650
+ jobsData.forEach(job => {
651
+ const row = document.createElement('tr');
652
+ row.className = 'hover:bg-gray-50';
653
+ row.innerHTML = `
654
+ <td class="px-6 py-4 whitespace-nowrap">
655
+ <div class="font-medium text-gray-900">${job.customer_name || 'N/A'}</div>
656
+ </td>
657
+ <td class="px-6 py-4 whitespace-nowrap">
658
+ <div class="text-gray-900">${job.job_id}</div>
659
+ </td>
660
+ <td class="px-6 py-4">
661
+ <div class="text-gray-900 max-w-xs truncate">${job.description || 'No description'}</div>
662
+ </td>
663
+ <td class="px-6 py-4 whitespace-nowrap">
664
+ <div class="text-gray-900">${job.quantity || 0}</div>
665
+ </td>
666
+ <td class="px-6 py-4 whitespace-nowrap">
667
+ <div class="text-gray-900">£${(job.price || 0).toFixed(2)}</div>
668
+ </td>
669
+ <td class="px-6 py-4 whitespace-nowrap">
670
+ <div class="font-medium text-gray-900">£${((job.quantity || 0) * (job.price || 0)).toFixed(2)}</div>
671
+ </td>
672
+ <td class="px-6 py-4 whitespace-nowrap">
673
+ <img src="${job.thumbnail_url || 'https://via.placeholder.com/100'}" alt="Thumbnail" class="w-12 h-12 object-cover rounded cursor-pointer thumbnail" data-image="${job.image_url || 'https://via.placeholder.com/800'}">
674
+ </td>
675
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
676
+ <i class="fas fa-edit text-gray-400 hover:text-blue-500 cursor-pointer edit-icon" data-job-id="${job.job_id}"></i>
677
+ </td>
678
+ `;
679
+ tableBody.appendChild(row);
680
+ });
681
+ }
682
+
683
+ // Render the invalid jobs table
684
+ function renderInvalidJobsTable() {
685
+ const tableBody = document.getElementById('invalid-jobs-table-body');
686
+ tableBody.innerHTML = '';
687
+
688
+ if (invalidJobsData.length === 0) {
689
+ const row = document.createElement('tr');
690
+ row.innerHTML = `
691
+ <td colspan="6" class="px-6 py-4 text-center text-gray-500">
692
+ No invalid jobs found
693
+ </td>
694
+ `;
695
+ tableBody.appendChild(row);
696
+ return;
697
+ }
698
+
699
+ invalidJobsData.forEach(job => {
700
+ const row = document.createElement('tr');
701
+ row.className = 'hover:bg-gray-50 invalid-row';
702
+ row.innerHTML = `
703
+ <td class="px-6 py-4 whitespace-nowrap">
704
+ <div class="font-medium text-gray-900">${job.customer_name || 'N/A'}</div>
705
+ </td>
706
+ <td class="px-6 py-4 whitespace-nowrap">
707
+ <div class="text-gray-900">${job.job_id}</div>
708
+ </td>
709
+ <td class="px-6 py-4">
710
+ <div class="text-gray-900 max-w-xs truncate">${job.description || 'No description'}</div>
711
+ </td>
712
+ <td class="px-6 py-4 whitespace-nowrap">
713
+ <div class="text-red-600">${job.missing_info || 'Unknown issue'}</div>
714
+ </td>
715
+ <td class="px-6 py-4 whitespace-nowrap">
716
+ <img src="${job.thumbnail_url || 'https://via.placeholder.com/100'}" alt="Thumbnail" class="w-12 h-12 object-cover rounded cursor-pointer thumbnail" data-image="${job.image_url || 'https://via.placeholder.com/800'}">
717
+ </td>
718
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
719
+ <i class="fas fa-edit text-gray-400 hover:text-blue-500 cursor-pointer edit-icon" data-job-id="${job.job_id}"></i>
720
+ </td>
721
+ `;
722
+ tableBody.appendChild(row);
723
+ });
724
+ }
725
+
726
+ // Initialize the trend chart
727
+ let trendChart;
728
+ function initTrendChart(timeframe = 'week') {
729
+ const ctx = document.getElementById('trendChart').getContext('2d');
730
+
731
+ let labels, data;
732
+ switch(timeframe) {
733
+ case 'month':
734
+ labels = monthlyTrendData.map(item => item.month);
735
+ data = monthlyTrendData.map(item => item.total);
736
+ break;
737
+ case 'quarter':
738
+ labels = quarterlyTrendData.map(item => item.quarter);
739
+ data = quarterlyTrendData.map(item => item.total);
740
+ break;
741
+ case 'year':
742
+ labels = yearlyTrendData.map(item => item.year);
743
+ data = yearlyTrendData.map(item => item.total);
744
+ break;
745
+ default: // week
746
+ labels = weeklyTrendData.map(item => item.week);
747
+ data = weeklyTrendData.map(item => item.total);
748
+ }
749
+
750
+ if (trendChart) {
751
+ trendChart.destroy();
752
+ }
753
+
754
+ trendChart = new Chart(ctx, {
755
+ type: 'bar',
756
+ data: {
757
+ labels: labels,
758
+ datasets: [{
759
+ label: 'Revenue',
760
+ data: data,
761
+ backgroundColor: 'rgba(59, 130, 246, 0.7)',
762
+ borderColor: 'rgba(59, 130, 246, 1)',
763
+ borderWidth: 1
764
+ }]
765
+ },
766
+ options: {
767
+ responsive: true,
768
+ maintainAspectRatio: false,
769
+ plugins: {
770
+ legend: {
771
+ display: false
772
+ },
773
+ tooltip: {
774
+ callbacks: {
775
+ label: function(context) {
776
+ return `£${context.raw.toLocaleString()}`;
777
+ }
778
+ }
779
+ }
780
+ },
781
+ scales: {
782
+ y: {
783
+ beginAtZero: false,
784
+ ticks: {
785
+ callback: function(value) {
786
+ return `£${value.toLocaleString()}`;
787
+ }
788
+ },
789
+ grid: {
790
+ drawBorder: false
791
+ }
792
+ },
793
+ x: {
794
+ grid: {
795
+ display: false
796
+ }
797
+ }
798
+ }
799
+ }
800
+ });
801
+ }
802
+
803
+ // Initialize the pie chart
804
+ function initPieChart() {
805
+ const customerCounts = {};
806
+ jobsData.forEach(job => {
807
+ const customer = job.customer_name || 'Unknown';
808
+ customerCounts[customer] = (customerCounts[customer] || 0) + 1;
809
+ });
810
+
811
+ const customers = Object.keys(customerCounts);
812
+ const counts = Object.values(customerCounts);
813
+
814
+ const colors = [
815
+ 'rgba(59, 130, 246, 0.7)',
816
+ 'rgba(16, 185, 129, 0.7)',
817
+ 'rgba(245, 158, 11, 0.7)',
818
+ 'rgba(139, 92, 246, 0.7)',
819
+ 'rgba(220, 38, 38, 0.7)'
820
+ ];
821
+
822
+ const ctx = document.getElementById('pieChart').getContext('2d');
823
+ new Chart(ctx, {
824
+ type: 'pie',
825
+ data: {
826
+ labels: customers,
827
+ datasets: [{
828
+ data: counts,
829
+ backgroundColor: colors,
830
+ borderWidth: 1
831
+ }]
832
+ },
833
+ options: {
834
+ responsive: true,
835
+ maintainAspectRatio: false,
836
+ plugins: {
837
+ legend: {
838
+ position: 'right',
839
+ },
840
+ tooltip: {
841
+ callbacks: {
842
+ label: function(context) {
843
+ const label = context.label || '';
844
+ const value = context.raw || 0;
845
+ const total = context.dataset.data.reduce((a, b) => a + b, 0);
846
+ const percentage = Math.round((value / total) * 100);
847
+ return `${label}: ${value} job${value !== 1 ? 's' : ''} (${percentage}%)`;
848
+ }
849
+ }
850
+ }
851
+ }
852
+ }
853
+ });
854
+ }
855
+
856
+ // Show error message
857
+ function showError(message) {
858
+ const errorAlert = document.getElementById('error-alert');
859
+ const errorMessage = document.getElementById('error-message');
860
+
861
+ errorMessage.textContent = message;
862
+ errorAlert.classList.remove('hidden');
863
+
864
+ setTimeout(() => {
865
+ errorAlert.classList.add('hidden');
866
+ }, 5000);
867
+ }
868
+
869
+ // Set up all event listeners
870
+ function setupEventListeners() {
871
+ // Image modal
872
+ document.addEventListener('click', function(e) {
873
+ if (e.target.classList.contains('thumbnail')) {
874
+ const imageUrl = e.target.getAttribute('data-image');
875
+ document.getElementById('modal-image').src = imageUrl;
876
+ document.getElementById('image-modal').classList.remove('opacity-0', 'pointer-events-none');
877
+ }
878
+ });
879
+
880
+ // Close modal
881
+ document.getElementById('close-modal').addEventListener('click', function() {
882
+ document.getElementById('image-modal').classList.add('opacity-0', 'pointer-events-none');
883
+ });
884
+
885
+ // Close error alert
886
+ document.getElementById('close-error').addEventListener('click', function() {
887
+ document.getElementById('error-alert').classList.add('hidden');
888
+ });
889
+
890
+ // Download image
891
+ document.getElementById('download-image').addEventListener('click', function() {
892
+ const imageUrl = document.getElementById('modal-image').src;
893
+ const link = document.createElement('a');
894
+ link.href = imageUrl;
895
+ link.download = 'job-image.jpg';
896
+ document.body.appendChild(link);
897
+ link.click();
898
+ document.body.removeChild(link);
899
+ });
900
+
901
+ // Edit job
902
+ document.addEventListener('click', function(e) {
903
+ if (e.target.classList.contains('edit-icon')) {
904
+ const jobId = e.target.getAttribute('data-job-id');
905
+ const job = [...jobsData, ...invalidJobsData].find(j => j.job_id === jobId);
906
+
907
+ if (job) {
908
+ document.getElementById('edit-job-id').value = job.job_id;
909
+ document.getElementById('edit-job-id-display').value = job.job_id;
910
+ document.getElementById('edit-customer').value = job.customer_name || '';
911
+ document.getElementById('edit-description').value = job.description || '';
912
+ document.getElementById('edit-quantity').value = job.quantity || '';
913
+ document.getElementById('edit-price').value = job.price || '';
914
+ document.getElementById('edit-total').value = (job.quantity || 0) * (job.price || 0);
915
+ document.getElementById('edit-thumbnail').src = job.thumbnail_url || 'https://via.placeholder.com/100';
916
+
917
+ document.getElementById('edit-modal').classList.remove('opacity-0', 'pointer-events-none');
918
+ }
919
+ }
920
+ });
921
+
922
+ // Close edit modal
923
+ document.getElementById('close-edit-modal').addEventListener('click', function() {
924
+ document.getElementById('edit-modal').classList.add('opacity-0', 'pointer-events-none');
925
+ });
926
+
927
+ document.getElementById('cancel-edit').addEventListener('click', function() {
928
+ document.getElementById('edit-modal').classList.add('opacity-0', 'pointer-events-none');
929
+ });
930
+
931
+ // Calculate total when quantity or price changes
932
+ document.getElementById('edit-quantity').addEventListener('input', calculateTotal);
933
+ document.getElementById('edit-price').addEventListener('input', calculateTotal);
934
+
935
+ function calculateTotal() {
936
+ const quantity = parseFloat(document.getElementById('edit-quantity').value) || 0;
937
+ const price = parseFloat(document.getElementById('edit-price').value) || 0;
938
+ const total = quantity * price;
939
+ document.getElementById('edit-total').value = total.toFixed(2);
940
+ }
941
+
942
+ // Save job
943
+ document.getElementById('save-job').addEventListener('click', async function() {
944
+ const jobId = document.getElementById('edit-job-id').value;
945
+ const customer = document.getElementById('edit-customer').value;
946
+ const description = document.getElementById('edit-description').value;
947
+ const quantity = parseFloat(document.getElementById('edit-quantity').value) || 0;
948
+ const price = parseFloat(document.getElementById('edit-price').value) || 0;
949
+
950
+ try {
951
+ const response = await fetch(`${API_BASE_URL}/jobs/${jobId}`, {
952
+ method: 'PUT',
953
+ headers: {
954
+ 'Content-Type': 'application/json'
955
+ },
956
+ body: JSON.stringify({
957
+ customer_name: customer,
958
+ description: description,
959
+ quantity: quantity,
960
+ price: price
961
+ })
962
+ });
963
+
964
+ if (!response.ok) throw new Error('Failed to update job');
965
+
966
+ // Refresh data
967
+ await fetchAllData();
968
+ renderJobsTable();
969
+ renderInvalidJobsTable();
970
+ renderStatsCards();
971
+
972
+ document.getElementById('edit-modal').classList.add('opacity-0', 'pointer-events-none');
973
+ } catch (error) {
974
+ console.error('Error updating job:', error);
975
+ showError('Failed to update job. Please try again.');
976
+ }
977
+ });
978
+
979
+ // Delete job
980
+ document.getElementById('delete-job').addEventListener('click', async function() {
981
+ if (!confirm('Are you sure you want to delete this job?')) return;
982
+
983
+ const jobId = document.getElementById('edit-job-id').value;
984
+
985
+ try {
986
+ const response = await fetch(`${API_BASE_URL}/jobs/${jobId}`, {
987
+ method: 'DELETE'
988
+ });
989
+
990
+ if (!response.ok) throw new Error('Failed to delete job');
991
+
992
+ // Refresh data
993
+ await fetchAllData();
994
+ renderJobsTable();
995
+ renderInvalidJobsTable();
996
+ renderStatsCards();
997
+
998
+ document.getElementById('edit-modal').classList.add('opacity-0', 'pointer-events-none');
999
+ } catch (error) {
1000
+ console.error('Error deleting job:', error);
1001
+ showError('Failed to delete job. Please try again.');
1002
+ }
1003
+ });
1004
+
1005
+ // This Week button
1006
+ document.getElementById('this-week-btn').addEventListener('click', async function() {
1007
+ setCurrentWeek();
1008
+
1009
+ try {
1010
+ // Show loading overlay
1011
+ document.getElementById('loading-overlay').classList.remove('hidden');
1012
+
1013
+ // Fetch jobs for current week
1014
+ const today = new Date();
1015
+ const dayOfWeek = today.getDay();
1016
+ const monday = new Date(today);
1017
+ monday.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1));
1018
+ const friday = new Date(monday);
1019
+ friday.setDate(monday.getDate() + 4);
1020
+
1021
+ const response = await fetch(`${API_BASE_URL}/jobs?status=completed&startDate=${moment(monday).format('YYYY-MM-DD')}&endDate=${moment(friday).format('YYYY-MM-DD')}`);
1022
+ if (!response.ok) throw new Error('Failed to fetch jobs');
1023
+
1024
+ jobsData = await response.json();
1025
+ renderJobsTable();
1026
+
1027
+ // Hide loading overlay
1028
+ document.getElementById('loading-overlay').classList.add('hidden');
1029
+ } catch (error) {
1030
+ console.error('Error fetching jobs:', error);
1031
+ document.getElementById('loading-overlay').classList.add('hidden');
1032
+ showError('Failed to load jobs for current week.');
1033
+ }
1034
+ });
1035
+
1036
+ // Search functionality
1037
+ document.getElementById('search').addEventListener('input', function() {
1038
+ const searchTerm = this.value.toLowerCase();
1039
+
1040
+ if (searchTerm.length < 2) {
1041
+ renderJobsTable();
1042
+ return;
1043
+ }
1044
+
1045
+ const filteredJobs = jobsData.filter(job =>
1046
+ (job.customer_name && job.customer_name.toLowerCase().includes(searchTerm)) ||
1047
+ (job.job_id && job.job_id.toLowerCase().includes(searchTerm)) ||
1048
+ (job.description && job.description.toLowerCase().includes(searchTerm))
1049
+ );
1050
+
1051
+ const tableBody = document.getElementById('jobs-table-body');
1052
+ tableBody.innerHTML = '';
1053
+
1054
+ if (filteredJobs.length === 0) {
1055
+ const row = document.createElement('tr');
1056
+ row.innerHTML = `
1057
+ <td colspan="8" class="px-6 py-4 text-center text-gray-500">
1058
+ No jobs match your search criteria
1059
+ </td>
1060
+ `;
1061
+ tableBody.appendChild(row);
1062
+ return;
1063
+ }
1064
+
1065
+ filteredJobs.forEach(job => {
1066
+ const row = document.createElement('tr');
1067
+ row.className = 'hover:bg-gray-50';
1068
+ row.innerHTML = `
1069
+ <td class="px-6 py-4 whitespace-nowrap">
1070
+ <div class="font-medium text-gray-900">${job.customer_name || 'N/A'}</div>
1071
+ </td>
1072
+ <td class="px-6 py-4 whitespace-nowrap">
1073
+ <div class="text-gray-900">${job.job_id}</div>
1074
+ </td>
1075
+ <td class="px-6 py-4">
1076
+ <div class="text-gray-900 max-w-xs truncate">${job.description || 'No description'}</div>
1077
+ </td>
1078
+ <td class="px-6 py-4 whitespace-nowrap">
1079
+ <div class="text-gray-900">${job.quantity || 0}</div>
1080
+ </td>
1081
+ <td class="px-6 py-4 whitespace-nowrap">
1082
+ <div class="text-gray-900">£${(job.price || 0).toFixed(2)}</div>
1083
+ </td>
1084
+ <td class="px-6 py-4 whitespace-nowrap">
1085
+ <div class="font-medium text-gray-900">£${((job.quantity || 0) * (job.price || 0)).toFixed(2)}</div>
1086
+ </td>
1087
+ <td class="px-6 py-4 whitespace-nowrap">
1088
+ <img src="${job.thumbnail_url || 'https://via.placeholder.com/100'}" alt="Thumbnail" class="w-12 h-12 object-cover rounded cursor-pointer thumbnail" data-image="${job.image_url || 'https://via.placeholder.com/800'}">
1089
+ </td>
1090
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
1091
+ <i class="fas fa-edit text-gray-400 hover:text-blue-500 cursor-pointer edit-icon" data-job-id="${job.job_id}"></i>
1092
+ </td>
1093
+ `;
1094
+ tableBody.appendChild(row);
1095
+ });
1096
+ });
1097
+
1098
+ // Column sorting
1099
+ document.querySelectorAll('.sortable').forEach(header => {
1100
+ header.addEventListener('click', function() {
1101
+ const column = this.getAttribute('data-column');
1102
+
1103
+ // Toggle sort direction indicator
1104
+ document.querySelectorAll('.sortable').forEach(h => {
1105
+ h.classList.remove('sort-asc', 'sort-desc');
1106
+ });
1107
+
1108
+ const isAsc = this.classList.contains('sort-asc');
1109
+ this.classList.remove('sort-asc', 'sort-desc');
1110
+ this.classList.add(isAsc ? 'sort-desc' : 'sort-asc');
1111
+
1112
+ // Sort the data
1113
+ jobsData.sort((a, b) => {
1114
+ let valA = a[column] || '';
1115
+ let valB = b[column] || '';
1116
+
1117
+ // Handle numeric columns
1118
+ if (column === 'quantity' || column === 'price' || column === 'total') {
1119
+ valA = parseFloat(valA) || 0;
1120
+ valB = parseFloat(valB) || 0;
1121
+ return isAsc ? valB - valA : valA - valB;
1122
+ }
1123
+
1124
+ // Handle string columns
1125
+ valA = String(valA).toLowerCase();
1126
+ valB = String(valB).toLowerCase();
1127
+ return isAsc ?
1128
+ valB.localeCompare(valA) :
1129
+ valA.localeCompare(valB);
1130
+ });
1131
+
1132
+ renderJobsTable();
1133
+ });
1134
+ });
1135
+
1136
+ // Timeframe buttons for trend chart
1137
+ document.querySelectorAll('.timeframe-btn').forEach(btn => {
1138
+ btn.addEventListener('click', function() {
1139
+ const timeframe = this.getAttribute('data-timeframe');
1140
+
1141
+ // Update active state
1142
+ document.querySelectorAll('.timeframe-btn').forEach(b => {
1143
+ b.classList.remove('active');
1144
+ });
1145
+ this.classList.add('active');
1146
+
1147
+ // Update chart
1148
+ initTrendChart(timeframe);
1149
+ });
1150
+ });
1151
+
1152
+ // Add Job button
1153
+ document.getElementById('add-job-btn').addEventListener('click', function() {
1154
+ // Reset form
1155
+ document.getElementById('add-job-form').reset();
1156
+ document.getElementById('add-thumbnail').src = 'https://via.placeholder.com/100';
1157
+
1158
+ // Show modal
1159
+ document.getElementById('add-modal').classList.remove('opacity-0', 'pointer-events-none');
1160
+ });
1161
+
1162
+ // Close add modal
1163
+ document.getElementById('close-add-modal').addEventListener('click', function() {
1164
+ document.getElementById('add-modal').classList.add('opacity-0', 'pointer-events-none');
1165
+ });
1166
+
1167
+ document.getElementById('cancel-add').addEventListener('click', function() {
1168
+ document.getElementById('add-modal').classList.add('opacity-0', 'pointer-events-none');
1169
+ });
1170
+
1171
+ // Calculate total for add form
1172
+ document.getElementById('add-quantity').addEventListener('input', calculateAddTotal);
1173
+ document.getElementById('add-price').addEventListener('input', calculateAddTotal);
1174
+
1175
+ function calculateAddTotal() {
1176
+ const quantity = parseFloat(document.getElementById('add-quantity').value) || 0;
1177
+ const price = parseFloat(document.getElementById('add-price').value) || 0;
1178
+ const total = quantity * price;
1179
+ document.getElementById('add-total').value = total.toFixed(2);
1180
+ }
1181
+
1182
+ // Save new job
1183
+ document.getElementById('save-new-job').addEventListener('click', async function() {
1184
+ const customer = document.getElementById('add-customer').value;
1185
+ const jobId = document.getElementById('add-job-id').value;
1186
+ const description = document.getElementById('add-description').value;
1187
+ const quantity = parseFloat(document.getElementById('add-quantity').value) || 0;
1188
+ const price = parseFloat(document.getElementById('add-price').value) || 0;
1189
+ const thumbnailUrl = document.getElementById('add-thumbnail').src;
1190
+
1191
+ // Simple validation
1192
+ if (!customer || !jobId || !description) {
1193
+ showError('Please fill in all required fields');
1194
+ return;
1195
+ }
1196
+
1197
+ try {
1198
+ // Show loading overlay
1199
+ document.getElementById('loading-overlay').classList.remove('hidden');
1200
+
1201
+ // Insert new job
1202
+ const response = await fetch(`${API_BASE_URL}/jobs`, {
1203
+ method: 'POST',
1204
+ headers: {
1205
+ 'Content-Type': 'application/json'
1206
+ },
1207
+ body: JSON.stringify({
1208
+ job_id: jobId,
1209
+ customer_name: customer,
1210
+ description: description,
1211
+ quantity: quantity,
1212
+ price: price,
1213
+ thumbnail_url: thumbnailUrl,
1214
+ image_url: thumbnailUrl.replace('100', '800'),
1215
+ status: 'completed'
1216
+ })
1217
+ });
1218
+
1219
+ if (!response.ok) throw new Error('Failed to add job');
1220
+
1221
+ // Refresh data
1222
+ await fetchAllData();
1223
+ renderJobsTable();
1224
+ renderInvalidJobsTable();
1225
+ renderStatsCards();
1226
+
1227
+ // Hide modals and loading overlay
1228
+ document.getElementById('add-modal').classList.add('opacity-0', 'pointer-events-none');
1229
+ document.getElementById('loading-overlay').classList.add('hidden');
1230
+
1231
+ showError('Job added successfully!');
1232
+ } catch (error) {
1233
+ console.error('Error adding job:', error);
1234
+ document.getElementById('loading-overlay').classList.add('hidden');
1235
+ showError('Failed to add job. Please try again.');
1236
+ }
1237
+ });
1238
+ }
1239
+ </script>
1240
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=TheHoodedFoot/psql-nodejs-dashboard" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
1241
+ </html>
prompts.txt ADDED
File without changes