TheHoodedFoot commited on
Commit
6fd62d3
·
verified ·
1 Parent(s): 81a5e20

undefined - Initial Deployment

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