Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/pr-notification.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Slack PR Notification

on:
pull_request:
types: [opened]

jobs:
notify-slack:
runs-on: ubuntu-latest
steps:
- name: Send Slack Notification
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
"text": "New Pull Request: ${{ github.event.pull_request.title }}",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "🔔 New Pull Request"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Title:*\n${{ github.event.pull_request.title }}"
},
{
"type": "mrkdwn",
"text": "*Author:*\n${{ github.event.pull_request.user.login }}"
},
{
"type": "mrkdwn",
"text": "*Repository:*\n${{ github.repository }}"
},
{
"type": "mrkdwn",
"text": "*Branch:*\n${{ github.event.pull_request.head.ref }}"
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "<${{ github.event.pull_request.html_url }}|View Pull Request>"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
70 changes: 70 additions & 0 deletions client/src/Components/IssueFilters.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
export const IssueFilters = ({ filters, onFilterChange }) => {
return (
<div className="flex gap-4 mb-4">
{/* Status filter */}
<select
value={filters.status}
onChange={(e) => onFilterChange("status", e.target.value)}
className="border rounded px-3 py-2"
>
<option value="all">All Status</option>
<option value="OPEN">Open</option>
<option value="IN_PROGRESS">In Progress</option>
<option value="RESOLVED">Resolved</option>
<option value="CLOSED">Closed</option>
</select>

{/* Priority filter */}
<select
value={filters.priority}
onChange={(e) => onFilterChange("priority", e.target.value)}
className="border rounded px-3 py-2"
>
<option value="all">All Priority</option>
<option value="LOW">Low</option>
<option value="MEDIUM">Medium</option>
<option value="HIGH">High</option>
<option value="URGENT">Urgent</option>
</select>

{/* Category filter */}
<select
value={filters.category}
onChange={(e) => onFilterChange("category", e.target.value)}
className="border rounded px-3 py-2"
>
<option value="all">All Categories</option>
<option value="PLUMBING">Plumbing</option>
<option value="HVAC">HVAC</option>
<option value="ELECTRICAL">Electrical</option>
<option value="APPLIANCE">Appliance</option>
<option value="STRUCTURAL">Structural</option>
<option value="PEST_CONTROL">Pest Control</option>
<option value="LOCKS_KEYS">Locks & Keys</option>
<option value="FLOORING">Flooring</option>
<option value="WALLS_CEILING">Walls & Ceiling</option>
<option value="WINDOWS_DOORS">Windows & Doors</option>
<option value="LANDSCAPING">Landscaping</option>
<option value="PARKING">Parking</option>
<option value="OTHER">Other</option>
</select>

{/* Date filter */}
<div className="flex items-center">
<input
type="date"
value={filters.startDate}
onChange={(e) => onFilterChange("startDate", e.target.value)}
className="border rounded px-3 py-2 mr-2"
/>
<span className="mx-2">to</span>
<input
type="date"
value={filters.endDate}
onChange={(e) => onFilterChange("endDate", e.target.value)}
className="border rounded px-3 py-2"
/>
</div>
</div>
);
};
59 changes: 33 additions & 26 deletions client/src/Components/PhotoUpload.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { photoAPI } from '../services/api';
import { useState } from "react";
import { photoAPI } from "../services/api";

export const PhotoUpload = ({ issueId, onUploadComplete }) => {
const [selectedFiles, setSelectedFiles] = useState([]);
Expand All @@ -13,13 +13,14 @@ export const PhotoUpload = ({ issueId, onUploadComplete }) => {
const fileArray = Array.from(files);

// Validate files
const validFiles = fileArray.filter(file => {
if (!file.type.startsWith('image/')) {
setUploadError('Only image files are allowed');
const validFiles = fileArray.filter((file) => {
if (!file.type.startsWith("image/")) {
setUploadError("Only image files are allowed");
return false;
}
if (file.size > 5 * 1024 * 1024) { // 5MB limit
setUploadError('Files must be less than 5MB');
if (file.size > 5 * 1024 * 1024) {
// 5MB limit
setUploadError("Files must be less than 5MB");
return false;
}
return true;
Expand All @@ -37,9 +38,9 @@ export const PhotoUpload = ({ issueId, onUploadComplete }) => {

try {
// Get presigned URLs
const fileMetadata = selectedFiles.map(file => ({
const fileMetadata = selectedFiles.map((file) => ({
name: file.name,
type: file.type
type: file.type,
}));

const { presignedUrls } = await photoAPI.getPresignedUrls(fileMetadata);
Expand All @@ -50,9 +51,9 @@ export const PhotoUpload = ({ issueId, onUploadComplete }) => {
const { uploadUrl } = presignedUrls[index];

const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
method: "PUT",
body: file,
headers: { 'Content-Type': file.type }
headers: { "Content-Type": file.type },
});

if (!uploadResponse.ok) {
Expand All @@ -67,30 +68,32 @@ export const PhotoUpload = ({ issueId, onUploadComplete }) => {
fileName: file.name,
fileSize: file.size,
mimeType: file.type,
caption: ''
caption: "",
}));

const saveResponse = await fetch(`${import.meta.env.VITE_API_URL}/photos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
photos: photoMetadata,
issueId: issueId
})
});
const saveResponse = await fetch(
`${import.meta.env.VITE_API_URL}/photos`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
photos: photoMetadata,
issueId: issueId,
}),
}
);

if (!saveResponse.ok) {
throw new Error('Failed to save photo metadata');
throw new Error("Failed to save photo metadata");
}

// Clear selection and refresh
setSelectedFiles([]);
const savedPhotos = await saveResponse.json();
onUploadComplete(savedPhotos);

} catch (err) {
console.error('Upload failed:', err);
setUploadError(err.message || 'Upload failed. Please try again.');
console.error("Upload failed:", err);
setUploadError(err.message || "Upload failed. Please try again.");
} finally {
setUploading(false);
}
Expand All @@ -104,7 +107,7 @@ export const PhotoUpload = ({ issueId, onUploadComplete }) => {
multiple
accept="image/*"
onChange={handleFileSelect}
className="hidden"
style={{ display: "none" }}
id="photo-upload"
disabled={uploading}
/>
Expand Down Expand Up @@ -147,7 +150,11 @@ export const PhotoUpload = ({ issueId, onUploadComplete }) => {
disabled={uploading}
className="bg-green-600 text-white px-6 py-2 rounded-md hover:bg-green-700 transition-colors disabled:bg-gray-400"
>
{uploading ? 'Uploading...' : `Upload ${selectedFiles.length} Photo${selectedFiles.length > 1 ? 's' : ''}`}
{uploading
? "Uploading..."
: `Upload ${selectedFiles.length} Photo${
selectedFiles.length > 1 ? "s" : ""
}`}
</button>
)}
</div>
Expand Down
Loading