Added rest of files, first commit from code server

This commit is contained in:
2024-11-30 20:26:30 -05:00
parent ffbc67abcf
commit e94ef0d68a
29 changed files with 2277 additions and 16589 deletions

1
.gitignore vendored Normal file → Executable file
View File

@ -17,6 +17,7 @@
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
.env
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*

View File

@ -1,70 +0,0 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

16291
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +0,0 @@
{
"name": "tinkertickets",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -1,25 +0,0 @@
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;

View File

@ -1,8 +0,0 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

50
src/add_comment.php Normal file
View File

@ -0,0 +1,50 @@
<?php
// Load environment variables
$envFile = __DIR__ . '/.env';
$envVars = parse_ini_file($envFile);
// Database connection
$conn = new mysqli(
$envVars['REACT_APP_DB_HOST'],
$envVars['REACT_APP_DB_USER'],
$envVars['REACT_APP_DB_PASS'],
$envVars['REACT_APP_DB_NAME']
);
// Get POST data
$data = json_decode(file_get_contents('php://input'), true);
// Set default username (you can modify this based on your user system)
$username = "User";
// Prepare insert query
$sql = "INSERT INTO ticket_comments (ticket_id, user_name, comment_text, markdown_enabled) VALUES (?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
// Convert markdown_enabled to integer for database
$markdownEnabled = $data['markdown_enabled'] ? 1 : 0;
$stmt->bind_param("sssi",
$data['ticket_id'],
$username,
$data['comment_text'],
$markdownEnabled
);
if ($stmt->execute()) {
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'user_name' => $username,
'created_at' => date('M d, Y H:i'),
'markdown_enabled' => $markdownEnabled
]);
} else {
echo json_encode([
'success' => false,
'error' => $conn->error
]);
}
$stmt->close();
$conn->close();

View File

@ -0,0 +1,553 @@
/* Variables */
:root {
--bg-primary: #f5f7fa;
--bg-secondary: #ffffff;
--text-primary: #2c3e50;
--text-secondary: #4a5568;
--border-color: #eee;
--shadow: 0 2px 4px rgba(0,0,0,0.1);
--hover-bg: #f8f9fa;
--priority-1: #ff4d4d;
--priority-2: #ffa726;
--priority-3: #42a5f5;
--priority-4: #66bb6a;
/* Spacing */
--spacing-xs: 0.5rem;
--spacing-sm: 1rem;
--spacing-md: 1.5rem;
--spacing-lg: 2rem;
/* Transitions */
--transition-default: all 0.3s ease;
}
/* Dark theme */
[data-theme="dark"] {
--bg-primary: #1a202c;
--bg-secondary: #2d3748;
--text-primary: #f7fafc;
--text-secondary: #e2e8f0;
--border-color: #4a5568;
--shadow: 0 2px 4px rgba(0,0,0,0.3);
--hover-bg: #374151;
--priority-1: #7f1d1d;
--priority-2: #854d0e;
--priority-3: #075985;
--priority-4: #166534;
}
/* Base Elements */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: var(--spacing-md);
background-color: var(--bg-primary);
color: var(--text-primary);
transition: var(--transition-default);
}
/* Reusable Components */
.card-base {
background: var(--bg-secondary);
border-radius: 12px;
box-shadow: var(--shadow);
padding: var(--spacing-md);
}
.btn-base {
padding: var(--spacing-sm) var(--spacing-md);
border-radius: 6px;
border: none;
cursor: pointer;
transition: var(--transition-default);
}
.btn-primary {
background: #3b82f6;
color: white;
}
/* Layout Components */
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
margin-left: 3.75rem;
}
.ticket-container {
max-width: 800px;
margin: var(--spacing-lg) auto;
border-left: 6px solid;
transition: var(--transition-default);
}
.flex-row {
display: flex;
gap: var(--spacing-sm);
}
.flex-between {
justify-content: space-between;
align-items: center;
}
/* Table Styles */
.table-base {
width: 100%;
border-collapse: separate;
border-spacing: 0;
overflow: hidden;
}
.table-cell {
padding: var(--spacing-md);
text-align: left;
border-bottom: 1px solid var(--border-color);
}
/* Priority Styles */
.priority-indicator {
font-weight: bold;
font-family: 'Courier New', monospace;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: 4px;
display: inline-block;
}
.priority-1 { color: var(--priority-1); }
.priority-2 { color: var(--priority-2); }
.priority-3 { color: var(--priority-3); }
.priority-4 { color: var(--priority-4); }
/* Status Styles */
.status-base {
font-weight: bold;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: 4px;
}
.status-Open {
color: #10b981;
background: rgba(16, 185, 129, 0.1);
}
.status-Closed {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
/*UNCHECKED BELOW*/
body.menu-open {
padding-left: 260px;
}
.create-ticket {
background: #3b82f6;
color: white;
padding: 0.625rem 1.25rem;
border-radius: 0.375rem;
border: none;
cursor: pointer;
font-weight: 500;
transition: background-color 0.3s ease;
margin-right: 3.75rem;
}
.create-ticket:hover {
background: #2563eb;
}
h1 {
color: var(--text-primary);
margin: 0;
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
background: var(--bg-secondary);
border-radius: 12px;
box-shadow: var(--shadow);
overflow: hidden;
}
th, td {
padding: 16px;
text-align: left;
border-bottom: 1px solid var(--border-color);
color: var(--text-primary);
}
th {
background-color: var(--bg-secondary);
font-weight: 600;
text-transform: uppercase;
font-size: 0.9em;
letter-spacing: 0.05em;
}
tr:hover {
background-color: var(--hover-bg);
}
tbody tr td:first-child {
border-left: 6px solid;
}
tbody tr.priority-1 td:first-child { border-left-color: var(--priority-1); }
tbody tr.priority-2 td:first-child { border-left-color: var(--priority-2); }
tbody tr.priority-3 td:first-child { border-left-color: var(--priority-3); }
tbody tr.priority-4 td:first-child { border-left-color: var(--priority-4); }
/* Priority number styling */
td:nth-child(2) {
text-align: center;
}
td:nth-child(2) span {
font-weight: bold;
font-family: 'Courier New', monospace;
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
background: var(--hover-bg);
}
.priority-1 td:nth-child(2) { color: var(--priority-1); }
.priority-2 td:nth-child(2) { color: var(--priority-2); }
.priority-3 td:nth-child(2) { color: var(--priority-3); }
.priority-4 td:nth-child(2) { color: var(--priority-4); }
.search-box {
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
background: var(--bg-secondary);
color: var(--text-primary);
margin-left: 1.25rem;
width: 40%;
}
.status-filter {
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
background: var(--bg-secondary);
color: var(--text-primary);
cursor: pointer;
min-width: 120px;
margin-right: 1rem;
}
.search-box:focus,
.status-filter:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.theme-toggle {
position: absolute;
top: 20px;
right: 20px;
z-index: 100;
padding: 12px;
border-radius: 50%;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
cursor: pointer;
box-shadow: var(--shadow);
font-size: 1.2em;
transition: transform 0.3s ease;
}
.theme-toggle:hover {
transform: scale(1.1);
}
.table-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 10px;
background: var(--bg-secondary);
border-radius: 8px;
box-shadow: var(--shadow);
}
.ticket-count {
font-weight: 500;
color: var(--text-secondary);
}
.table-actions {
display: flex;
gap: 15px;
align-items: center;
}
.pagination {
display: flex;
gap: 0.5rem;
align-items: center;
}
.pagination button {
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
background: var(--bg-secondary);
color: var(--text-primary);
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s ease;
}
.pagination button:hover {
background: var(--hover-bg);
}
.pagination button.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.settings-icon {
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: background-color 0.2s;
}
.settings-icon:hover {
background: var(--hover-bg);
}
/* Settings Modal Styles */
.settings-modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.settings-modal {
background: var(--bg-secondary);
border-radius: 12px;
width: 500px;
max-width: 90%;
box-shadow: var(--shadow);
padding: 20px;
}
.settings-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-color);
padding-bottom: 15px;
margin-bottom: 15px;
}
.settings-modal-header h2 {
margin: 0;
color: var(--text-primary);
}
.close-modal {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--text-secondary);
}
.setting-group {
margin-bottom: 15px;
}
.setting-group label {
display: block;
margin-bottom: 8px;
color: var(--text-primary);
}
.setting-group select {
width: 100%;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
}
.settings-modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
border-top: 1px solid var(--border-color);
padding-top: 15px;
}
.save-settings, .cancel-settings {
padding: 10px 20px;
border-radius: 6px;
border: none;
cursor: pointer;
transition: background-color 0.3s ease;
}
.save-settings {
background: #3b82f6;
color: white;
}
.cancel-settings {
background: var(--hover-bg);
color: var(--text-primary);
}
/* Sorting indicator styles */
th {
position: relative; /* Ensure proper positioning of arrows */
cursor: pointer; /* Show it's clickable */
}
th::after {
content: '';
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
opacity: 0.5; /* Make arrows less prominent when not active */
}
th.sort-asc::after {
border-bottom: 7px solid var(--text-primary);
opacity: 1;
}
th.sort-desc::after {
border-top: 7px solid var(--text-primary);
opacity: 1;
}
/* Column toggle styles */
.column-toggles {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.column-toggles label {
display: flex;
align-items: center;
gap: 8px;
}
.hamburger-menu {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
}
.hamburger-icon {
cursor: pointer;
font-size: 24px;
background: var(--bg-secondary);
padding: 10px;
border-radius: 4px;
box-shadow: var(--shadow);
}
.hamburger-content {
position: fixed;
top: 0;
left: -250px;
width: 200px;
height: 100%;
background: var(--bg-secondary);
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
transition: left 0.3s ease, margin-left 0.3s ease;
padding: 40px 20px 20px;
overflow-y: auto;
z-index: 99;
}
.hamburger-content.open {
left: 0;
}
.close-hamburger {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
font-size: 24px;
background: var(--bg-secondary);
padding: 10px;
border-radius: 4px;
box-shadow: var(--shadow);
}
.filter-section {
margin-bottom: 20px;
}
.filter-section label {
display: block;
margin-bottom: 10px;
}
.filter-actions {
display: flex;
gap: 10px;
}
.filter-actions button {
flex: 1;
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
#apply-filters {
background: #3b82f6;
color: white;
}
#clear-filters {
background: var(--hover-bg);
color: var(--text-primary);
}
.ticket-link {
font-family: 'Courier New', monospace;
font-weight: bold;
color: var(--text-primary) !important;
text-decoration: none;
background: var(--hover-bg);
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
}
.ticket-link:hover {
background: var(--border-color);
}

413
src/assets/css/ticket.css Normal file
View File

@ -0,0 +1,413 @@
/* Base Layout Components */
.ticket-container {
max-width: 800px;
margin: 40px auto;
padding: 20px;
background: var(--bg-secondary);
border-radius: 12px;
box-shadow: var(--shadow);
border-left: 6px solid;
transition: border-color 0.3s ease;
}
.full-width {
grid-column: 1 / -1;
}
/* Header Components */
.ticket-header {
display: flex;
flex-direction: column;
margin-bottom: 30px;
}
.ticket-subheader {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.header-controls {
display: flex;
gap: 15px;
align-items: center;
}
.ticket-id {
font-family: 'Courier New', monospace;
margin-right: 20px;
}
h1 {
margin: 0;
padding: 0;
width: 100%;
display: block;
}
/* Title Input Styles */
.title-input {
font-size: 1em;
font-weight: bold;
width: 100%;
border: 2px solid transparent;
border-radius: 4px;
padding: 4px;
margin: -4px;
word-break: break-word;
white-space: normal;
display: block;
}.title-input:not(:disabled) {
border-color: var(--border-color);
background: var(--bg-primary);
}
.title-input:not(:disabled):hover {
border-color: #3b82f6;
}
.title-input:disabled {
color: var(--text-primary);
border: none;
background: transparent;
}
/* Form Elements */
.detail-group {
margin-bottom: 20px;
}
.detail-group label {
display: block;
margin-bottom: 8px;
color: var(--text-secondary);
font-weight: 500;
}
.editable {
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
}
input.editable {
width: calc(100% - 20px);
box-sizing: border-box;
}
textarea.editable {
width: calc(100% - 20px);
min-height: 150px;
resize: vertical;
box-sizing: border-box;
}
.editable:disabled {
background: var(--bg-secondary);
cursor: default;
border-color: transparent;
}
/* Button Styles */
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
background: var(--bg-primary);
color: var(--text-primary);
}
.btn.primary {
background: #3b82f6;
color: white;
}
.btn.active {
background: #3b82f6;
color: white;
}
/* Status and Priority Styles */
.status-priority-row {
display: flex;
gap: 20px;
}
.detail-half {
flex: 1;
}
.status-priority-group {
display: flex;
gap: 10px;
align-items: center;
margin-right: 15px;
}
.priority-indicator {
padding: 4px 8px;
border-radius: 4px;
font-weight: bold;
}
/* Priority Select Styles */
select[data-field="priority"] {
border-left: 4px solid;
}
select[data-field="priority"] option {
padding: 10px;
}
select[data-field="priority"] option[value="1"] {
background-color: var(--priority-1);
}
select[data-field="priority"] option[value="2"] {
background-color: var(--priority-2);
}
select[data-field="priority"] option[value="3"] {
background-color: var(--priority-3);
}
select[data-field="priority"] option[value="4"] {
background-color: var(--priority-4);
}
select[data-field="priority"][value="1"] {
border-left-color: var(--priority-1);
}
select[data-field="priority"][value="2"] {
border-left-color: var(--priority-2);
}
select[data-field="priority"][value="3"] {
border-left-color: var(--priority-3);
}
select[data-field="priority"][value="4"] {
border-left-color: var(--priority-4);
}
select[data-field="priority"] option[value="1"]:hover {
background-color: #ffc9c9;
color: var(--text-primary);
}
select[data-field="priority"] option[value="2"]:hover {
background-color: #ffe0b2;
color: var(--text-primary);
}
select[data-field="priority"] option[value="3"]:hover {
background-color: #bbdefb;
color: var(--text-primary);
}
select[data-field="priority"] option[value="4"]:hover {
background-color: #c8e6c9;
color: var(--text-primary);
}
[data-priority="1"] { border-color: var(--priority-1); }
[data-priority="2"] { border-color: var(--priority-2); }
[data-priority="3"] { border-color: var(--priority-3); }
[data-priority="4"] { border-color: var(--priority-4); }
/* Comments Section */
.comments-section {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
}
.comment-form {
margin-bottom: 20px;
}
.comment-form textarea {
width: calc(100% - 20px);
min-height: 80px;
margin-bottom: 10px;
padding: 10px;
border-radius: 6px;
border: 1px solid var(--border-color);
background: var(--bg-primary);
color: var(--text-primary);
}
.comment {
background: var(--bg-primary);
padding: 15px;
border-radius: 6px;
margin-bottom: 10px;
}
.comment-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 0.9em;
}
.comment-user {
font-weight: bold;
color: var(--text-primary);
}
.comment-date {
color: var(--text-secondary);
}
.comment-text {
color: var(--text-primary);
line-height: 1.4;
}
.comment-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin: 10px 0;
}
/* Comment Tabs */
.ticket-tabs {
display: flex;
gap: 10px;
margin: 20px 0;
}
.tab-btn {
padding: 10px 20px;
border: 1px solid var(--border-color);
background: var(--bg-secondary);
border-radius: 6px;
cursor: pointer;
font-weight: 500;
}
.tab-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Markdown Styles */
.markdown-preview {
min-height: 100px;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
}
.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3 {
margin-top: 0;
}
.markdown-preview code {
background: var(--bg-secondary);
padding: 2px 4px;
border-radius: 4px;
}
.markdown-preview pre {
background: var(--bg-secondary);
padding: 10px;
border-radius: 6px;
overflow-x: auto;
}
.markdown-toggles {
display: flex;
gap: 20px;
}
/* Toggle Switch */
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--bg-secondary);
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
}
.slider.round {
border-radius: 24px;
}
.slider.round:before {
border-radius: 50%;
}
input:checked + .slider {
background-color: #3b82f6;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.switch input:disabled + .slider {
opacity: 0.5;
cursor: not-allowed;
}
/* Footer */
.ticket-footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
}
.back-btn {
background: var(--bg-secondary);
color: var(--text-primary);
padding: 10px 20px;
border-radius: 6px;
border: 1px solid var(--border-color);
cursor: pointer;
transition: background-color 0.3s ease;
}
.back-btn:hover {
background: var(--border-color);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

487
src/assets/js/dashboard.js Normal file
View File

@ -0,0 +1,487 @@
document.addEventListener('DOMContentLoaded', function() {
// Only initialize filters if we're on the dashboard
if (document.querySelector('table')) {
initSearch();
initStatusFilter();
}
// Keep theme toggle for all pages
initThemeToggle();
// Load saved theme preference
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
// Add sorting functionality
const tableHeaders = document.querySelectorAll('th');
tableHeaders.forEach(header => {
header.addEventListener('click', () => {
const table = header.closest('table');
const index = Array.from(header.parentElement.children).indexOf(header);
sortTable(table, index);
});
});
// Add settings modal functionality
const settingsIcon = document.querySelector('.settings-icon');
if (settingsIcon) {
settingsIcon.addEventListener('click', function(e) {
e.preventDefault();
createSettingsModal();
});
}
});
function sortTable(table, column) {
// Remove existing sort indicators from all headers
const headers = table.querySelectorAll('th');
headers.forEach(header => {
header.classList.remove('sort-asc', 'sort-desc');
});
const rows = Array.from(table.querySelectorAll('tbody tr'));
// Determine current sort direction
const currentDirection = table.dataset.sortColumn === column
? (table.dataset.sortDirection === 'asc' ? 'desc' : 'asc')
: 'asc';
// Store current sorting column and direction
table.dataset.sortColumn = column;
table.dataset.sortDirection = currentDirection;
rows.sort((a, b) => {
const aValue = a.children[column].textContent.trim();
const bValue = b.children[column].textContent.trim();
// Try numeric sorting first, fallback to string comparison
const numA = parseFloat(aValue);
const numB = parseFloat(bValue);
if (!isNaN(numA) && !isNaN(numB)) {
return currentDirection === 'asc' ? numA - numB : numB - numA;
}
// String comparison
return currentDirection === 'asc'
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
});
// Add sort indicator to the current header
const currentHeader = headers[column];
currentHeader.classList.add(currentDirection === 'asc' ? 'sort-asc' : 'sort-desc');
// Reorder rows in the tbody
const tbody = table.querySelector('tbody');
rows.forEach(row => tbody.appendChild(row));
}
// Add this to the DOMContentLoaded event listener to persist sorting on page load
document.addEventListener('DOMContentLoaded', function() {
const table = document.querySelector('table');
if (table) {
const savedSortColumn = localStorage.getItem('sortColumn');
const savedSortDirection = localStorage.getItem('sortDirection');
if (savedSortColumn !== null && savedSortDirection !== null) {
const headers = table.querySelectorAll('th');
const columnIndex = Array.from(headers).findIndex(header =>
header.textContent.toLowerCase().replace(' ', '_') === savedSortColumn
);
if (columnIndex !== -1) {
table.dataset.sortColumn = columnIndex;
table.dataset.sortDirection = savedSortDirection;
const header = headers[columnIndex];
header.classList.add(savedSortDirection === 'asc' ? 'sort-asc' : 'sort-desc');
}
}
}
});
// Modify the existing event listeners for table headers
document.addEventListener('DOMContentLoaded', function() {
const tableHeaders = document.querySelectorAll('th');
tableHeaders.forEach((header, index) => {
header.addEventListener('click', () => {
const table = header.closest('table');
sortTable(table, index);
// Save sorting preferences
const columnName = header.textContent.toLowerCase().replace(' ', '_');
localStorage.setItem('sortColumn', columnName);
localStorage.setItem('sortDirection', table.dataset.sortDirection);
});
});
});
function createSettingsModal() {
// Create modal backdrop
const backdrop = document.createElement('div');
backdrop.className = 'settings-modal-backdrop';
backdrop.innerHTML = `
<div class="settings-modal">
<div class="settings-modal-header">
<h2>Dashboard Settings</h2>
<button class="close-modal">×</button>
</div>
<div class="settings-modal-content">
<div class="setting-group">
<h3>Toggle Columns</h3>
<div class="column-toggles">
<label>
<input type="checkbox" value="ticket_id" checked> Ticket ID
</label>
<label>
<input type="checkbox" value="title" checked> Title
</label>
<label>
<input type="checkbox" value="category" checked> Category
</label>
<label>
<input type="checkbox" value="type" checked> Type
</label>
<label>
<input type="checkbox" value="status" checked> Status
</label>
<label>
<input type="checkbox" value="priority" checked> Priority
</label>
<label>
<input type="checkbox" value="created" checked> Created
</label>
<label>
<input type="checkbox" value="updated" checked> Updated
</label>
</div>
</div>
<div class="setting-group">
<h3>Rows per Page</h3>
<select id="rows-per-page">
<option value="15">15</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
<div class="settings-modal-footer">
<button class="save-settings">Save Settings</button>
<button class="cancel-settings">Cancel</button>
</div>
</div>
`;
// Add to body
document.body.appendChild(backdrop);
// Load saved column visibility settings
const savedColumnSettings = JSON.parse(localStorage.getItem('columnVisibility') || '{}');
const checkboxes = backdrop.querySelectorAll('.column-toggles input');
checkboxes.forEach(checkbox => {
checkbox.checked = savedColumnSettings[checkbox.value] !== false;
});
// Load saved rows per page setting
const savedRowsPerPage = localStorage.getItem('ticketsPerPage') || '5';
const rowsPerPageSelect = backdrop.querySelector('#rows-per-page');
rowsPerPageSelect.value = savedRowsPerPage;
// Close modal events
backdrop.querySelector('.close-modal').addEventListener('click', closeSettingsModal);
backdrop.querySelector('.cancel-settings').addEventListener('click', closeSettingsModal);
backdrop.querySelector('.save-settings').addEventListener('click', saveSettings);
// Close modal on backdrop click
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) {
closeSettingsModal();
}
});
}
function closeSettingsModal() {
const backdrop = document.querySelector('.settings-modal-backdrop');
if (backdrop) {
backdrop.remove();
}
}
function saveSettings() {
// Save column visibility
const checkboxes = document.querySelectorAll('.column-toggles input');
const columnVisibility = {};
checkboxes.forEach(checkbox => {
columnVisibility[checkbox.value] = checkbox.checked;
});
localStorage.setItem('columnVisibility', JSON.stringify(columnVisibility));
// Save rows per page
const rowsPerPage = document.querySelector('#rows-per-page').value;
localStorage.setItem('ticketsPerPage', rowsPerPage);
// Set cookie for PHP to read
document.cookie = `ticketsPerPage=${rowsPerPage}; path=/`;
// Apply column visibility
applyColumnVisibility();
// Reload page to apply pagination changes
window.location.reload();
// Close modal
closeSettingsModal();
}
function applyColumnVisibility() {
const savedColumnSettings = JSON.parse(localStorage.getItem('columnVisibility') || '{}');
const table = document.querySelector('table');
if (table) {
const headers = table.querySelectorAll('th');
const rows = table.querySelectorAll('tbody tr');
headers.forEach((header, index) => {
const columnValue = header.textContent.toLowerCase().replace(' ', '_');
const isVisible = savedColumnSettings[columnValue] !== false;
header.style.display = isVisible ? '' : 'none';
rows.forEach(row => {
row.children[index].style.display = isVisible ? '' : 'none';
});
});
}
}
// Apply column visibility on page load
document.addEventListener('DOMContentLoaded', applyColumnVisibility);
// Dark mode toggle
function initThemeToggle() {
const toggle = document.createElement('button');
toggle.className = 'theme-toggle';
toggle.innerHTML = '🌓';
toggle.onclick = () => {
document.documentElement.setAttribute('data-theme',
document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'
);
localStorage.setItem('theme', document.documentElement.getAttribute('data-theme'));
};
document.body.appendChild(toggle);
}
// Search functionality
function initSearch() {
const searchBox = document.createElement('input');
searchBox.type = 'text';
searchBox.placeholder = 'Search tickets...';
searchBox.className = 'search-box';
searchBox.oninput = (e) => {
const searchTerm = e.target.value.toLowerCase();
const rows = document.querySelectorAll('tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(searchTerm) ? '' : 'none';
});
};
document.querySelector('h1').after(searchBox);
}
// Filter by status
function initStatusFilter() {
const filter = document.createElement('select');
filter.innerHTML = `
<option value="">All Status</option>
<option value="Open">Open</option>
<option value="Closed">Closed</option>
`;
filter.className = 'status-filter';
filter.onchange = (e) => {
const status = e.target.value;
const rows = document.querySelectorAll('tbody tr');
rows.forEach(row => {
if (!status || row.querySelector('.status-' + status)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
};
document.querySelector('.table-controls .table-actions').prepend(filter);
}
function sortTable(table, column) {
const headers = table.querySelectorAll('th');
headers.forEach(header => {
header.classList.remove('sort-asc', 'sort-desc');
});
const rows = Array.from(table.querySelectorAll('tbody tr'));
const currentDirection = table.dataset.sortColumn == column
? (table.dataset.sortDirection === 'asc' ? 'desc' : 'asc')
: 'asc';
table.dataset.sortColumn = column;
table.dataset.sortDirection = currentDirection;
rows.sort((a, b) => {
const aValue = a.children[column].textContent.trim();
const bValue = b.children[column].textContent.trim();
// Check if this is a date column (Created or Updated)
const headerText = headers[column].textContent.toLowerCase();
if (headerText === 'created' || headerText === 'updated') {
const dateA = new Date(aValue);
const dateB = new Date(bValue);
return currentDirection === 'asc' ? dateA - dateB : dateB - dateA;
}
// Existing numeric and string comparison logic
const numA = parseFloat(aValue);
const numB = parseFloat(bValue);
if (!isNaN(numA) && !isNaN(numB)) {
return currentDirection === 'asc' ? numA - numB : numB - numA;
}
return currentDirection === 'asc'
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
});
const currentHeader = headers[column];
currentHeader.classList.add(currentDirection === 'asc' ? 'sort-asc' : 'sort-desc');
const tbody = table.querySelector('tbody');
rows.forEach(row => tbody.appendChild(row));
}
// Modify the CSS to ensure arrows are more visible
document.addEventListener('DOMContentLoaded', function() {
const tableHeaders = document.querySelectorAll('th');
tableHeaders.forEach((header, index) => {
header.style.cursor = 'pointer'; // Make headers look clickable
header.addEventListener('click', () => {
const table = header.closest('table');
sortTable(table, index);
});
});
});
function createHamburgerMenu() {
// Create hamburger menu container
const hamburgerMenu = document.createElement('div');
hamburgerMenu.className = 'hamburger-menu';
hamburgerMenu.innerHTML = `
<div class="hamburger-icon">☰</div>
<div class="hamburger-content">
<div class="close-hamburger">☰</div>
<h3>Filters</h3>
<div class="filter-section">
<h4>Categories</h4>
<div id="category-filters"></div>
</div>
<div class="filter-section">
<h4>Types</h4>
<div id="type-filters"></div>
</div>
<div class="filter-actions">
<button id="apply-filters">Apply Filters</button>
<button id="clear-filters">Clear Filters</button>
</div>
</div>
`;
// Populate categories and types from data attributes
const categoriesContainer = hamburgerMenu.querySelector('#category-filters');
const typesContainer = hamburgerMenu.querySelector('#type-filters');
const categories = JSON.parse(document.body.dataset.categories || '[]');
const types = JSON.parse(document.body.dataset.types || '[]');
// Create checkboxes for categories
categories.forEach(category => {
const label = document.createElement('label');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = category;
checkbox.name = 'category';
label.appendChild(checkbox);
label.appendChild(document.createTextNode(category));
categoriesContainer.appendChild(label);
});
// Create checkboxes for types
types.forEach(type => {
const label = document.createElement('label');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = type;
checkbox.name = 'type';
label.appendChild(checkbox);
label.appendChild(document.createTextNode(type));
typesContainer.appendChild(label);
});
// Toggle hamburger menu
const hamburgerIcon = hamburgerMenu.querySelector('.hamburger-icon');
const hamburgerContent = hamburgerMenu.querySelector('.hamburger-content');
hamburgerIcon.addEventListener('click', () => {
hamburgerContent.classList.toggle('open');
document.body.classList.toggle('menu-open');
});
// Apply filters
const applyFiltersBtn = hamburgerMenu.querySelector('#apply-filters');
applyFiltersBtn.addEventListener('click', () => {
const selectedCategories = Array.from(
categoriesContainer.querySelectorAll('input:checked')
).map(cb => cb.value);
const selectedTypes = Array.from(
typesContainer.querySelectorAll('input:checked')
).map(cb => cb.value);
// Construct URL with filters
const params = new URLSearchParams(window.location.search);
if (selectedCategories.length > 0) {
params.set('category', selectedCategories.join(','));
} else {
params.delete('category');
}
if (selectedTypes.length > 0) {
params.set('type', selectedTypes.join(','));
} else {
params.delete('type');
}
// Reload with new filters
window.location.search = params.toString();
});
// Clear filters
const clearFiltersBtn = hamburgerMenu.querySelector('#clear-filters');
clearFiltersBtn.addEventListener('click', () => {
const params = new URLSearchParams(window.location.search);
params.delete('category');
params.delete('type');
window.location.search = params.toString();
});
// Add to body
document.body.appendChild(hamburgerMenu);
// Close hamburger menu
const closeButton = hamburgerMenu.querySelector('.close-hamburger');
closeButton.addEventListener('click', () => {
hamburgerContent.classList.remove('open');
document.body.classList.remove('menu-open');
});
}
// Add to DOMContentLoaded
document.addEventListener('DOMContentLoaded', createHamburgerMenu);

184
src/assets/js/ticket.js Normal file
View File

@ -0,0 +1,184 @@
function saveTicket() {
const editables = document.querySelectorAll('.editable');
const data = {};
const ticketId = window.location.href.split('id=')[1];
editables.forEach(field => {
if (field.dataset.field) {
data[field.dataset.field] = field.value;
}
});
fetch('update_ticket.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
ticket_id: ticketId,
...data
})
})
.then(response => response.json())
.then(data => {
if(data.success) {
const statusDisplay = document.getElementById('statusDisplay');
statusDisplay.className = `status-${data.status}`;
statusDisplay.textContent = data.status;
}
});
}
function toggleEditMode() {
const editButton = document.getElementById('editButton');
const editables = document.querySelectorAll('.editable');
const isEditing = editButton.classList.contains('active');
if (!isEditing) {
// Enable editing
editButton.textContent = 'Save Changes';
editButton.classList.add('active');
editables.forEach(field => {
field.disabled = false;
if (field.classList.contains('title-input')) {
field.focus();
}
});
} else {
// Save changes and disable editing
saveTicket();
editButton.textContent = 'Edit Ticket';
editButton.classList.remove('active');
editables.forEach(field => {
field.disabled = true;
});
}
}
function addComment() {
const commentText = document.getElementById('newComment').value;
const ticketId = window.location.href.split('id=')[1];
fetch('add_comment.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
ticket_id: ticketId,
comment_text: commentText
})
})
.then(response => response.json())
.then(data => {
if(data.success) {
// Clear the comment box
document.getElementById('newComment').value = '';
// Add new comment to the list
const commentsList = document.querySelector('.comments-list');
const newComment = `
<div class="comment">
<div class="comment-header">
<span class="comment-user">${data.user_name}</span>
<span class="comment-date">${data.created_at}</span>
</div>
<div class="comment-text">${commentText}</div>
</div>
`;
commentsList.insertAdjacentHTML('afterbegin', newComment);
}
});
}
function togglePreview() {
const preview = document.getElementById('markdownPreview');
const textarea = document.getElementById('newComment');
const isPreviewEnabled = document.getElementById('markdownToggle').checked;
preview.style.display = isPreviewEnabled ? 'block' : 'none';
if (isPreviewEnabled) {
preview.innerHTML = marked.parse(textarea.value);
textarea.addEventListener('input', updatePreview);
} else {
textarea.removeEventListener('input', updatePreview);
}
}
function updatePreview() {
const preview = document.getElementById('markdownPreview');
const textarea = document.getElementById('newComment');
preview.innerHTML = marked.parse(textarea.value);
}
function toggleMarkdownMode() {
const previewToggle = document.getElementById('markdownToggle');
const isMasterEnabled = document.getElementById('markdownMaster').checked;
previewToggle.disabled = !isMasterEnabled;
if (!isMasterEnabled) {
previewToggle.checked = false;
document.getElementById('markdownPreview').style.display = 'none';
}
}
function addComment() {
const commentText = document.getElementById('newComment').value;
const isMarkdownEnabled = document.getElementById('markdownMaster').checked;
const ticketId = window.location.href.split('id=')[1];
fetch('add_comment.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
ticket_id: ticketId,
comment_text: commentText,
markdown_enabled: isMarkdownEnabled
})
})
.then(response => response.json())
.then(data => {
if(data.success) {
const commentsList = document.querySelector('.comments-list');
const newCommentHtml = `
<div class="comment">
<div class="comment-header">
<span class="comment-user">${data.user_name}</span>
<span class="comment-date">${data.created_at}</span>
</div>
<div class="comment-text">
${isMarkdownEnabled ? marked.parse(commentText) : commentText}
</div>
</div>
`;
commentsList.insertAdjacentHTML('afterbegin', newCommentHtml);
document.getElementById('newComment').value = '';
}
});
}
document.addEventListener('DOMContentLoaded', function() {
// Show description tab by default
showTab('description');
});
function showTab(tabName) {
// Hide all tab contents
const descriptionTab = document.getElementById('description-tab');
const commentsTab = document.getElementById('comments-tab');
// Hide both tabs
descriptionTab.style.display = 'none';
commentsTab.style.display = 'none';
// Remove active class from all buttons
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
// Show selected tab and activate its button
document.getElementById(`${tabName}-tab`).style.display = 'block';
document.querySelector(`[onclick="showTab('${tabName}')"]`).classList.add('active');
}

84
src/create_ticket_api.php Normal file
View File

@ -0,0 +1,84 @@
<?php
header('Content-Type: application/json');
// Load environment variables with error check
$envFile = __DIR__ . '/.env';
if (!file_exists($envFile)) {
echo json_encode([
'success' => false,
'error' => 'Configuration file not found'
]);
exit;
}
$envVars = parse_ini_file($envFile);
if (!$envVars) {
echo json_encode([
'success' => false,
'error' => 'Invalid configuration file'
]);
exit;
}
// Database connection with detailed error handling
$conn = new mysqli(
$envVars['REACT_APP_DB_HOST'],
$envVars['REACT_APP_DB_USER'],
$envVars['REACT_APP_DB_PASS'],
$envVars['REACT_APP_DB_NAME']
);
if ($conn->connect_error) {
echo json_encode([
'success' => false,
'error' => 'Database connection failed: ' . $conn->connect_error
]);
exit;
}
// Get POST data
$data = json_decode(file_get_contents('php://input'), true);
// Generate ticket ID (9-digit format with leading zeros)
$ticket_id = sprintf('%09d', mt_rand(1, 999999999));
// Prepare insert query
$sql = "INSERT INTO tickets (ticket_id, title, description, status, priority, category, type)
VALUES (?, ?, ?, ?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
// First, store all values in variables
$title = $data['title'];
$description = $data['description'];
$status = $data['status'] ?? 'Open';
$priority = $data['priority'] ?? '4';
$category = $data['category'] ?? 'General';
$type = $data['type'] ?? 'Issue';
// Then use the variables in bind_param
$stmt->bind_param(
"sssssss",
$ticket_id,
$title,
$description,
$status,
$priority,
$category,
$type
);
if ($stmt->execute()) {
echo json_encode([
'success' => true,
'ticket_id' => $ticket_id,
'message' => 'Ticket created successfully'
]);
} else {
echo json_encode([
'success' => false,
'error' => $conn->error
]);
}
$stmt->close();
$conn->close();

149
src/dashboard.php Executable file
View File

@ -0,0 +1,149 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Load environment variables
$envFile = __DIR__ . '/.env';
$envVars = parse_ini_file($envFile);
// Database connection settings
$dbHost = $envVars['REACT_APP_DB_HOST'];
$dbUser = $envVars['REACT_APP_DB_USER'];
$dbPass = $envVars['REACT_APP_DB_PASS'];
$dbName = $envVars['REACT_APP_DB_NAME'];
// Create database connection
$conn = new mysqli($dbHost, $dbUser, $dbPass, $dbName);
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// Pagination settings from localStorage or defaults
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$limit = isset($_COOKIE['ticketsPerPage']) ? (int)$_COOKIE['ticketsPerPage'] : 15;
$defaultSortColumn = isset($_COOKIE['defaultSortColumn']) ? $_COOKIE['defaultSortColumn'] : 'ticket_id';
$sortDirection = isset($_COOKIE['sortDirection']) ? $_COOKIE['sortDirection'] : 'desc';
$offset = ($page - 1) * $limit;
// Get total number of tickets
$totalTicketsQuery = "SELECT COUNT(*) as total FROM tickets";
$totalTicketsResult = $conn->query($totalTicketsQuery);
$totalTickets = $totalTicketsResult->fetch_assoc()['total'];
$totalPages = ceil($totalTickets / $limit);
// Modify SQL to use these settings
$sql = "SELECT * FROM tickets ORDER BY $defaultSortColumn $sortDirection LIMIT $limit OFFSET $offset";
$result = $conn->query($sql);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ticket Dashboard</title>
<link rel="icon" type="image/png" href="assets/images/favicon.png">
<link rel="stylesheet" href="assets/css/dashboard.css">
<script src="assets/js/dashboard.js"></script>
</head>
<body>
<div class="dashboard-header">
<h1>Tinker Tickets</h1>
<button onclick="window.location.href='new_ticket.php'" class="btn create-ticket">New Ticket</button>
</div>
<div class="table-controls">
<div class="ticket-count">
Total Tickets: <?php echo $totalTickets; ?>
</div>
<div class="table-actions">
<div class="pagination">
<?php
// Previous page button
if ($page > 1) {
echo "<button onclick='window.location.href=\"?page=" . ($page - 1) . "\"'>«</button>";
}
// Page number buttons
for ($i = 1; $i <= $totalPages; $i++) {
$activeClass = ($i === $page) ? 'active' : '';
echo "<button class='$activeClass' onclick='window.location.href=\"?page=$i\"'>$i</button>";
}
// Next page button
if ($page < $totalPages) {
echo "<button onclick='window.location.href=\"?page=" . ($page + 1) . "\"'>»</button>";
}
?>
</div>
<div class="settings-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
</div>
</div>
</div>
<table>
<thead>
<tr>
<th>Ticket ID</th>
<th>Priority</th>
<th>Title</th>
<th>Category</th>
<th>Type</th>
<th>Status</th>
<th>Created</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
<?php
if ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
echo "<tr class='priority-{$row['priority']}'>";
echo "<td><a href='ticket.php?id={$row['ticket_id']}' class='ticket-link'>{$row['ticket_id']}</a></td>";
echo "<td><span>{$row['priority']}</span></td>";
echo "<td>{$row['title']}</td>";
echo "<td>{$row['category']}</td>";
echo "<td>{$row['type']}</td>";
echo "<td class='status-{$row['status']}'>{$row['status']}</td>";
echo "<td>" . date('Y-m-d H:i', strtotime($row['created_at'])) . "</td>";
echo "<td>" . date('Y-m-d H:i', strtotime($row['updated_at'])) . "</td>";
echo "</tr>";
}
} else {
echo "<tr><td colspan='8'>No tickets found</td></tr>";
}
?>
</tbody>
</table>
<?php $conn->close(); ?>
<script>
document.body.dataset.categories = JSON.stringify([
<?php
$categories = [];
mysqli_data_seek($categoriesResult, 0);
while($row = $categoriesResult->fetch_assoc()) {
$categories[] = "'" . $row['category'] . "'";
}
echo implode(',', $categories);
?>
]);
document.body.dataset.types = JSON.stringify([
<?php
$types = [];
mysqli_data_seek($typesResult, 0);
while($row = $typesResult->fetch_assoc()) {
$types[] = "'" . $row['type'] . "'";
}
echo implode(',', $types);
?>
]);
</script>
</body>
</html>

View File

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -1,17 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

0
src/logo.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

104
src/new_ticket.php Normal file
View File

@ -0,0 +1,104 @@
<?php
// Load environment variables
$envFile = __DIR__ . '/.env';
$envVars = parse_ini_file($envFile);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create New Ticket</title>
<link rel="icon" type="image/png" href="assets/images/favicon.png">
<link rel="stylesheet" href="assets/css/dashboard.css">
<link rel="stylesheet" href="assets/css/ticket.css">
<script src="assets/js/dashboard.js"></script>
</head>
<body>
<div class="ticket-container">
<div class="ticket-header">
<h1>Create New Ticket</h1>
</div>
<form id="newTicketForm" class="ticket-details">
<div class="detail-group">
<label>Title</label>
<input type="text" name="title" required class="editable">
</div>
<div class="detail-group status-priority-row">
<div class="detail-quarter">
<label>Status</label>
<select name="status" required class="editable">
<option value="Open">Open</option>
<option value="Closed">Closed</option>
</select>
</div>
<div class="detail-quarter">
<label>Priority</label>
<select name="priority" required class="editable">
<option value="1">P1 - Critical Impact</option>
<option value="2">P2 - High Impact</option>
<option value="3">P3 - Medium Impact</option>
<option value="4">P4 - Low Impact</option>
</select>
</div>
<div class="detail-quarter">
<label>Category</label>
<select name="category" required class="editable">
<option value="Hardware">Hardware</option>
<option value="Software">Software</option>
<option value="Network">Network</option>
<option value="Security">Security</option>
<option value="Other">Other</option>
</select>
</div>
<div class="detail-quarter">
<label>Type</label>
<select name="type" required class="editable">
<option value="Incident">Incident</option>
<option value="Request">Request</option>
<option value="Problem">Problem</option>
<option value="Task">Task</option>
</select>
</div>
</div>
<div class="detail-group full-width">
<label>Description</label>
<textarea name="description" required class="editable"></textarea>
</div>
<div class="form-actions">
<button type="button" onclick="window.location.href='dashboard.php'" class="btn">Cancel</button>
<button type="submit" class="btn create-ticket">Create Ticket</button>
</div>
</form>
</div>
<script>
document.getElementById('newTicketForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = {};
formData.forEach((value, key) => data[key] = value);
fetch('create_ticket_api.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if(data.success) {
window.location.href = 'ticket.php?id=' + data.ticket_id;
}
});
});
</script>
</body>
</html>

View File

@ -1,13 +0,0 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

196
src/ticket.php Normal file
View File

@ -0,0 +1,196 @@
<?php
// Load environment variables and DB connection (same as dashboard.php)
$envFile = __DIR__ . "/.env";
$envVars = parse_ini_file($envFile);
$conn = new mysqli(
$envVars["REACT_APP_DB_HOST"],
$envVars["REACT_APP_DB_USER"],
$envVars["REACT_APP_DB_PASS"],
$envVars["REACT_APP_DB_NAME"]
);
$ticket_id = $_GET["id"];
$sql = "SELECT * FROM tickets WHERE ticket_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("i", $ticket_id);
$stmt->execute();
$result = $stmt->get_result();
$ticket = $result->fetch_assoc();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ticket #<?php echo $ticket_id; ?></title>
<link rel="icon" type="image/png" href="assets/images/favicon.png">
<link rel="stylesheet" href="assets/css/dashboard.css">
<link rel="stylesheet" href="assets/css/ticket.css">
<script src="assets/js/dashboard.js"></script>
<script src="assets/js/ticket.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
</head>
<body>
<div class="ticket-container" data-priority="<?php echo $ticket[
"priority"
]; ?>">
<div class="ticket-header">
<h2><input type="text" class="editable title-input" value="<?php echo $ticket[
"title"
]; ?>" data-field="title" disabled></h2>
<div class="ticket-subheader">
<div class="ticket-id">UUID <?php echo $ticket_id; ?></div>
<div class="header-controls">
<div class="status-priority-group">
<span id="statusDisplay" class="status-<?php echo $ticket[
"status"
]; ?>"><?php echo $ticket["status"]; ?></span>
<span class="priority-indicator priority-<?php echo $ticket[
"priority"
]; ?>">P<?php echo $ticket["priority"]; ?></span>
</div>
<button id="editButton" class="btn" onclick="toggleEditMode()">Edit Ticket</button>
</div>
</div>
</div>
<div class="ticket-details">
<div class="detail-group status-priority-row">
<div class="detail-quarter">
<label>Status</label>
<select class="editable" data-field="status" disabled>
<option value="Open" <?php echo $ticket["status"] == "Open"
? "selected"
: ""; ?>>Open</option>
<option value="Closed" <?php echo $ticket["status"] ==
"Closed"
? "selected"
: ""; ?>>Closed</option>
</select>
</div>
<div class="detail-quarter">
<label>Priority</label>
<select class="editable" data-field="priority" disabled>
<option value="1" <?php echo $ticket["priority"] == 1
? "selected"
: ""; ?>>P1 - Critical Impact</option>
<option value="2" <?php echo $ticket["priority"] == 2
? "selected"
: ""; ?>>P2 - High Impact</option>
<option value="3" <?php echo $ticket["priority"] == 3
? "selected"
: ""; ?>>P3 - Medium Impact</option>
<option value="4" <?php echo $ticket["priority"] == 4
? "selected"
: ""; ?>>P4 - Low Impact</option>
</select>
</div>
<div class="detail-quarter">
<label>Category</label>
<select class="editable" data-field="category" disabled>
<option value="Hardware" <?php echo $ticket["category"] ==
"Hardware"
? "selected"
: ""; ?>>Hardware</option>
<option value="Software" <?php echo $ticket["category"] ==
"Software"
? "selected"
: ""; ?>>Software</option>
<option value="Network" <?php echo $ticket["category"] ==
"Network"
? "selected"
: ""; ?>>Network</option>
<option value="Security" <?php echo $ticket["category"] ==
"Security"
? "selected"
: ""; ?>>Security</option>
<option value="Other" <?php echo $ticket["category"] ==
"Other"
? "selected"
: ""; ?>>Other</option>
</select>
</div>
<div class="detail-quarter">
<label>Type</label>
<select class="editable" data-field="type" disabled>
<option value="Maintenance" <?php echo $ticket["type"] == "Maintenance" ? "selected" : ""; ?>>Maintenance</option>
<option value="Install" <?php echo $ticket["type"] == "Install" ? "selected" : ""; ?>>Install</option>
<option value="Task" <?php echo $ticket["type"] == "Task" ? "selected" : ""; ?>>Task</option>
<option value="Upgrade" <?php echo $ticket["type"] == "Upgrade" ? "selected" : ""; ?>>Upgrade</option>
</select>
</div>
</div>
<div class="ticket-tabs">
<button class="tab-btn active" onclick="showTab('description')">Description</button>
<button class="tab-btn" onclick="showTab('comments')">Comments</button>
</div>
<div id="description-tab" class="tab-content active">
<div class="detail-group full-width">
<label>Description</label>
<textarea class="editable" data-field="description" disabled><?php echo $ticket[
"description"
]; ?></textarea>
</div>
</div>
<div id="comments-tab" class="tab-content">
<div class="comments-section">
<h2>Comments</h2>
<div class="comment-form">
<textarea id="newComment" placeholder="Add a comment..."></textarea>
<div class="comment-controls">
<div class="markdown-toggles">
<div class="preview-toggle">
<label class="switch">
<input type="checkbox" id="markdownMaster" onchange="toggleMarkdownMode()">
<span class="slider round"></span>
</label>
<span class="toggle-label">Enable Markdown</span>
</div>
<div class="preview-toggle">
<label class="switch">
<input type="checkbox" id="markdownToggle" onchange="togglePreview()" disabled>
<span class="slider round"></span>
</label>
<span class="toggle-label">Preview Markdown</span>
</div>
</div>
<button onclick="addComment()" class="btn">Add Comment</button>
</div>
<div id="markdownPreview" class="markdown-preview" style="display: none;"></div>
</div>
</div>
<div class="comments-list">
<?php
$commentsSql = "SELECT * FROM ticket_comments WHERE ticket_id = ? ORDER BY created_at DESC";
$stmt = $conn->prepare($commentsSql);
$stmt->bind_param("s", $ticket_id);
$stmt->execute();
$comments = $stmt->get_result();
while($comment = $comments->fetch_assoc()) {
echo "<div class='comment'>";
echo "<div class='comment-header'>";
echo "<span class='comment-user'>{$comment['user_name']}</span>";
echo "<span class='comment-date'>" . date('M d, Y H:i', strtotime($comment['created_at'])) . "</span>";
echo "</div>";
echo "<div class='comment-text'>";
if ($comment['markdown_enabled']) {
echo "<script>document.write(marked.parse(" . json_encode($comment['comment_text']) . "))</script>";
} else {
echo htmlspecialchars($comment['comment_text']);
}
echo "</div>";
echo "</div>";
}
?>
</div>
</div>
</div>
<div class="ticket-footer">
<button onclick="window.location.href='dashboard.php'" class="btn back-btn">Back to Dashboard</button>
</div>
</div>
</body>
</html>

56
src/update_ticket.php Normal file
View File

@ -0,0 +1,56 @@
<?php
// Load environment variables
$envFile = __DIR__ . '/.env';
$envVars = parse_ini_file($envFile);
// Database connection
$conn = new mysqli(
$envVars['REACT_APP_DB_HOST'],
$envVars['REACT_APP_DB_USER'],
$envVars['REACT_APP_DB_PASS'],
$envVars['REACT_APP_DB_NAME']
);
// Get POST data
$data = json_decode(file_get_contents('php://input'), true);
// Prepare update query
$sql = "UPDATE tickets SET
title = ?,
priority = ?,
status = ?,
description = ?,
category = ?,
type = ?,
updated_at = NOW()
WHERE ticket_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param(
"sisssss",
$data['title'],
$data['priority'],
$data['status'],
$data['description'],
$data['category'],
$data['type'],
$data['ticket_id']
);
// After successful update
if ($stmt->execute()) {
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'status' => $data['status'] // Send back the new status
]);
} else {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'error' => $conn->error
]);
}
$stmt->close();
$conn->close();