31 Commits
Author SHA1 Message Date
drake 1879150e72 kotlin upgrade 5 2024-06-28 13:43:03 -05:00
drake c233d71119 kotlin upgrade 4 2024-06-28 13:28:40 -05:00
drake 1e215ee86d kotlin upgrade 3 2024-06-28 13:18:08 -05:00
drake b8d2fc208b kotlin upgrade 2 2024-06-28 13:01:16 -05:00
drake a91364a5d7 kotlin upgrade 2024-06-28 12:46:02 -05:00
drake db8f44234a more fixes 2024-06-27 21:50:36 -05:00
drake 921ce17736 Api change 2024-06-27 12:07:59 -05:00
drake 4ce022d23d clearbit logo proxy 2 + sign in page 2024-06-27 11:56:27 -05:00
drake a875a1430d clearbit logo proxy 2024-06-27 11:35:26 -05:00
drake 0360736af0 api change for weird cors v3 2024-06-27 11:12:00 -05:00
drake 1802461f77 api change for weird cors v2 2024-06-27 10:49:52 -05:00
drake b92626b677 api change for weird cors 2024-06-27 10:41:34 -05:00
drake 1a6bf08bde more fixes 2024-06-26 21:35:47 -05:00
drake 1f7b850d66 formatting changes for presentation 2024-06-26 20:59:30 -05:00
drake 577801423d more fixes 2024-06-26 20:33:05 -05:00
drake a97ec0411d sign in page changes 2024-06-26 20:22:32 -05:00
drake 02bce8318a more fixes 2 2024-06-26 16:01:37 -05:00
drake 5614888454 more fixes 2024-06-26 13:30:02 -05:00
drake fd94dbf20d fixes 2024-06-26 13:06:03 -05:00
drake c4758eac73 api Change and catch change 2024-06-25 18:37:54 -05:00
drake 1e45bd173b api Change 2024-06-25 18:37:14 -05:00
drake e1f8c15e9a More fixes 2024-06-25 18:27:53 -05:00
drake 3cdf3b54ed API change and README updates 2024-06-24 20:24:52 -05:00
drake c65e225291 fixes 2024-06-23 18:56:22 -05:00
drake b860ae52f6 more fixes and features 2024-06-23 17:02:19 -05:00
drake 03abc1191d 0.2 fixes 2024-06-23 14:36:18 -05:00
drake 4517ec3078 v0.2.0 beta - Major screen changes 2024-06-20 13:20:32 -05:00
drake d72ee93f29 v0.2.0 beta - Major screen changes 2024-06-20 13:20:32 -05:00
drake 95b2e0bf11 update api address 2024-06-17 16:56:35 +00:00
drake 32e3cc574c Major Job Listings refactor 2024-06-16 14:04:12 -05:00
drake 9076765aae update api single business 2024-06-13 18:09:22 -05:00
30 changed files with 5482 additions and 2861 deletions
+30 -11
View File
@@ -1,10 +1,10 @@
# Job Link # Job Link
This is my app `Job Link` for the 2023-2024 FBLA Coding and Programming event.\ This is my app `Job Link` for the 2023-2024 FBLA Coding and Programming event.\
WI SLC Winner. WI SLC Winner.
It uses the [Flutter Framework](https://flutter.dev/) for the front end and the [Dart Language](https://dart.dev/) for both the front end and API. It uses the [Flutter Framework](https://flutter.dev/) for the front end and
the [Dart Language](https://dart.dev/) for both the front end and API.
## 3rd Party Libraries ## 3rd Party Libraries
@@ -16,24 +16,32 @@ It uses the [Flutter Framework](https://flutter.dev/) for the front end and the
- [open_filex](https://pub.dev/packages/open_filex) - [open_filex](https://pub.dev/packages/open_filex)
- [postgres](https://pub.dev/packages/postgres) - [postgres](https://pub.dev/packages/postgres)
- [argon2](https://pub.dev/packages/argon2) - [argon2](https://pub.dev/packages/argon2)
- [rive](https://pub.dev/packages/rive)
[Clearbit's logo API](https://clearbit.com/logo) was also used to get logos for businesses.
## Requirements ## Requirements
### Job Link ### Job Link
- **OS**: Windows, Linux, MacOS, Android, IOS. Note that some releases may not contain a MacOS or IOS build.
- **OS**: Windows, Linux, MacOS, Android, IOS. Note that some releases may not contain a MacOS or
IOS build.
- Stable internet connection. - Stable internet connection.
### API ### API
- **OS**: Windows, Linux, MacOS. - **OS**: Windows, Linux, MacOS.
- Stable internet connection. - Stable internet connection.
## Installation/Usage ## Installation/Usage
Please view the README in [fbla_ui](fbla_ui/README.md) and [fbla-api](fbla-api/README.md) for specific instructions for installation and usage for each part of the app. Please view the README in [fbla_ui](fbla_ui/README.md) and [fbla-api](fbla-api/README.md) for
specific instructions for installation and usage for each part of the app.
## Competitions ## Competitions
[Here](https://docs.google.com/presentation/d/1ZbSE9RqobU2T-NDIm3CUtT_9nEhOm3_B47NZl1-c_QA) is the presentation used for competitions. [Here](https://docs.google.com/presentation/d/1ZbSE9RqobU2T-NDIm3CUtT_9nEhOm3_B47NZl1-c_QA) is the
presentation used for competitions.
### WI State Leadership Conference ### WI State Leadership Conference
@@ -44,13 +52,24 @@ Used release 0.1.1\
Questions asked (with my answers): Questions asked (with my answers):
**Q**: Why did you decide to use the apps (framework and tools I assume) you used?\ **Q**: Why did you decide to use the apps (framework and tools I assume) you used?\
**A**: I decided to use Flutter primarily because of its cross-platform capabilities and its integration with the [Material design specification](https://m3.material.io/). I also decided to use it because I had some previous experience with Flutter and Dart, and Dart is somewhat similar to Java which I also had experience with. **A**: I decided to use Flutter primarily because of its cross-platform capabilities and its
integration with the [Material design specification](https://m3.material.io/). I also decided to use
it because I had some previous experience with Flutter and Dart, and Dart is somewhat similar to
Java which I also had experience with.
This one seems to just be a clarification for the `User input is validated` category of the [scoring sheet](https://connect.fbla.org/headquarters/files/High%20School%20Competitive%20Events%20Resources/Individual%20Guidelines/Presentation%20Events/Coding--Programming.pdf) (page 5-6)\ This one seems to just be a clarification for the `User input is validated` category of
**Q**: You mentioned that you validated that the description [of the business] is required, do you validate all the inputs or just that?\ the [scoring sheet](https://connect.fbla.org/headquarters/files/High%20School%20Competitive%20Events%20Resources/Individual%20Guidelines/Presentation%20Events/Coding--Programming.pdf) (
**A**: I validate that all of the required fields are inputted, and I also check for a valid email address and website format. page 5-6)\
**Q**: You mentioned that you validated that the description [of the business] is required, do you
validate all the inputs or just that?\
**A**: I validate that all of the required fields are inputted, and I also check for a valid email
address and website format.
This seems to be for the [scoring sheet](https://connect.fbla.org/headquarters/files/High%20School%20Competitive%20Events%20Resources/Individual%20Guidelines/Presentation%20Events/Coding--Programming.pdf) (page 5-6) `Code Quality` section.\ This seems to be for
the [scoring sheet](https://connect.fbla.org/headquarters/files/High%20School%20Competitive%20Events%20Resources/Individual%20Guidelines/Presentation%20Events/Coding--Programming.pdf) (
page 5-6) `Code Quality` section.\
While looking through a binder of the presentation and source code:\ While looking through a binder of the presentation and source code:\
**Q**: Is your source code all properly commented?\ **Q**: Is your source code all properly commented?\
**A**: While it is hard to comment UI code because of the nature of it, I did my best to use comments where applicable, and I also provide extensive documentation of the installation and usage of the app in the documentation in the README files. **A**: While it is hard to comment UI code because of the nature of it, I did my best to use
comments where applicable, and I also provide extensive documentation of the installation and usage
of the app in the documentation in the README files.
+55 -7
View File
@@ -4,13 +4,16 @@ This is the API for my 2023-2024 FBLA Coding & Programming App
1. Install [Dart SDK](https://dart.dev/get-dart). 1. Install [Dart SDK](https://dart.dev/get-dart).
2. Clone the repo . 2. Clone the repo .
```bash ```bash
git clone https://git.marinodev.com/MarinoDev/FBLA24.git git clone https://git.marinodev.com/MarinoDev/FBLA24.git
cd FBLA24/fbla-api/ cd FBLA24/fbla-api/
``` ```
3. Run `dart pub install` to install dart packages. 3. Run `dart pub install` to install dart packages.
4. Install [PostgreSQL](https://www.postgresql.org/) and set it up. 4. Install [PostgreSQL](https://www.postgresql.org/) and set it up.
5. Create `fbla` database in postgres. 5. Create `fbla` database in postgres.
```SQL ```SQL
CREATE DATABASE fbla CREATE DATABASE fbla
WITH WITH
@@ -19,9 +22,11 @@ CREATE DATABASE fbla
CONNECTION LIMIT = -1 CONNECTION LIMIT = -1
IS_TEMPLATE = False; IS_TEMPLATE = False;
``` ```
Make sure to change [username] to the actual username of your postgres instance. Make sure to change [username] to the actual username of your postgres instance.
6. Create `businesses` table. 6. Create `businesses` table.
```SQL ```SQL
-- Table: public.businesses -- Table: public.businesses
@@ -48,9 +53,39 @@ TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.businesses ALTER TABLE IF EXISTS public.businesses
OWNER to [username]; OWNER to [username];
``` ```
Make sure to change [username] to the actual username of your postgres instance. Make sure to change [username] to the actual username of your postgres instance.
7. Create `users` table. 7. Create `listings` table.
```SQL
-- Table: public.listings
-- DROP TABLE IF EXISTS public.listings;
CREATE TABLE IF NOT EXISTS public.listings
(
id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
"businessId" integer NOT NULL,
name text COLLATE pg_catalog."default" NOT NULL,
description text COLLATE pg_catalog."default" NOT NULL,
type text COLLATE pg_catalog."default" NOT NULL,
wage text COLLATE pg_catalog."default",
link text COLLATE pg_catalog."default",
"offerType" text COLLATE pg_catalog."default" NOT NULL,
CONSTRAINT listing_pkey PRIMARY KEY (id)
)
TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.listings
OWNER to [username];
```
Make sure to change [username] to the actual username of your postgres instance.
8. Create `users` table.
```SQL ```SQL
-- Table: public.users -- Table: public.users
@@ -68,11 +103,19 @@ CREATE TABLE IF NOT EXISTS public.users
TABLESPACE pg_default; TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.users ALTER TABLE IF EXISTS public.users
OWNER to postgres; OWNER to [username];
``` ```
8. Set environment variables `JOBLINK_POSTGRES_ADDRESS` (IP address), `JOBLINK_POSTGRES_PORT` (default 5432), `JOBLINK_POSTGRES_USERNAME`, and `JOBLINK_POSTGRES_PASSWORD` to appropriate information for your postgres database. Also set `JOBLINK_SECRET_KEY` to anything you want to generate JSON Web Tokens.
9. Set lib/create_first_user.dart username and password variables at top of file and run with `dart run lib/create_first_user.dart`.\ Make sure to change [username] to the actual username of your postgres instance.
Note: the username and password you use here will be what you use to log into the app as an admin.
8. Set environment variables `JOBLINK_POSTGRES_ADDRESS` (IP address), `JOBLINK_POSTGRES_PORT` (
default 5432), `JOBLINK_POSTGRES_USERNAME`, and `JOBLINK_POSTGRES_PASSWORD` to appropriate
information for your postgres database. Also set `JOBLINK_SECRET_KEY` to anything you want to
generate JSON Web Tokens.
9. Set lib/create_first_user.dart username and password variables at top of file and run
with `dart run lib/create_first_user.dart`.\
Note: the username and password you use here will be what you use to log into the app as an
admin.
## Usage ## Usage
@@ -83,9 +126,14 @@ Note the address it is serving at, you will need this to connect it to the UI, a
### Docker ### Docker
Run it in a docker container with [Docker Build](https://docs.docker.com/reference/cli/docker/image/build/) and [Docker Compose](https://docs.docker.com/compose/) using the included [Dockerfile](Dockerfile) and [docker-compose.yml](docker-compose.yml) files. If using `Docker Compose`, change the first portion of the volumes path on line 9 to any desired storage location.\ Run it in a docker container
with [Docker Build](https://docs.docker.com/reference/cli/docker/image/build/)
and [Docker Compose](https://docs.docker.com/compose/) using the included [Dockerfile](Dockerfile)
and [docker-compose.yml](docker-compose.yml) files. If using `Docker Compose`, change the first
portion of the volumes path on line 9 to any desired storage location.\
Note the address it is serving at, you will need this to connect it to the UI, and to run tests. Note the address it is serving at, you will need this to connect it to the UI, and to run tests.
### Testing ### Testing
You can test all functionality of your api with `dart run test/fbla_api_test.dart`, setting `apiAddress` on line 8 to your api address. You can test all functionality of your api with `dart run test/fbla_api_test.dart`,
setting `apiAddress` on line 8 to your api address.
+8 -7
View File
@@ -1,14 +1,17 @@
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:argon2/argon2.dart'; import 'package:argon2/argon2.dart';
import 'dart:io';
import 'package:postgres/postgres.dart'; import 'package:postgres/postgres.dart';
// Set these to the desired username and password of your user
String username = 'admin'; String username = 'admin';
String password = 'password'; String password = 'adminPassword';
var r = Random.secure(); var r = Random.secure();
String randomSalt = String.fromCharCodes(List.generate(32, (index) => r.nextInt(33) + 89)); String randomSalt =
String.fromCharCodes(List.generate(32, (index) => r.nextInt(33) + 89));
final salt = randomSalt.toBytesLatin1(); final salt = randomSalt.toBytesLatin1();
var parameters = Argon2Parameters( var parameters = Argon2Parameters(
@@ -37,10 +40,8 @@ Future<void> main() async {
argon2.generateBytes(passwordBytes, result); argon2.generateBytes(passwordBytes, result);
var resultHex = result.toHexString(); var resultHex = result.toHexString();
postgres.query( postgres.query('''
'''
INSERT INTO public.users (username, password_hash, salt) INSERT INTO public.users (username, password_hash, salt)
VALUES ('$username', '$resultHex', '$randomSalt') VALUES ('$username', '$resultHex', '$randomSalt')
''' ''');
);
} }
+247 -23
View File
@@ -22,12 +22,25 @@ enum BusinessType {
other, other,
} }
enum JobType { cashier, server, mechanic, other } enum JobType {
retail,
customerService,
foodService,
finance,
healthcare,
education,
maintenance,
manufacturing,
other,
}
enum OfferType { job, internship, apprenticeship }
class Business { class Business {
int id; int id;
String name; String name;
String description; String description;
BusinessType? type;
String? website; String? website;
String? contactName; String? contactName;
String? contactEmail; String? contactEmail;
@@ -40,6 +53,7 @@ class Business {
{required this.id, {required this.id,
required this.name, required this.name,
required this.description, required this.description,
this.type,
this.website, this.website,
this.contactName, this.contactName,
this.contactEmail, this.contactEmail,
@@ -49,11 +63,21 @@ class Business {
this.locationAddress}); this.locationAddress});
factory Business.fromJson(Map<String, dynamic> json) { factory Business.fromJson(Map<String, dynamic> json) {
bool typeValid = true;
try {
BusinessType.values.byName(json['type']);
} catch (e) {
typeValid = false;
}
return Business( return Business(
id: json['id'], id: json['id'],
name: json['name'], name: json['name'],
description: json['description'], description: json['description'],
website: json['website'], website: json['website'],
type: typeValid
? BusinessType.values.byName(json['type'])
: BusinessType.other,
contactName: json['contactName'], contactName: json['contactName'],
contactEmail: json['contactEmail'], contactEmail: json['contactEmail'],
contactPhone: json['contactPhone'], contactPhone: json['contactPhone'],
@@ -65,13 +89,14 @@ class Business {
} }
class JobListing { class JobListing {
String? id; int? id;
String? businessId; int? businessId;
String name; String name;
String description; String description;
JobType type; JobType type;
String? wage; String? wage;
String? link; String? link;
OfferType offerType;
JobListing( JobListing(
{this.id, {this.id,
@@ -80,7 +105,8 @@ class JobListing {
required this.description, required this.description,
required this.type, required this.type,
this.wage, this.wage,
this.link}); this.link,
required this.offerType});
factory JobListing.fromJson(Map<String, dynamic> json) { factory JobListing.fromJson(Map<String, dynamic> json) {
bool typeValid = true; bool typeValid = true;
@@ -98,7 +124,7 @@ class JobListing {
type: typeValid ? JobType.values.byName(json['type']) : JobType.other, type: typeValid ? JobType.values.byName(json['type']) : JobType.other,
wage: json['wage'], wage: json['wage'],
link: json['link'], link: json['link'],
); offerType: OfferType.values.byName(json['offerType']));
} }
} }
@@ -142,6 +168,15 @@ void main() async {
final app = Router(); final app = Router();
// CORS preflight acceptor
app.options('/<ignored|.*>', (Request request) {
return Response.ok(null, headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': '*',
});
});
// routes // routes
app.get('/fbla-api/hello', (Request request) async { app.get('/fbla-api/hello', (Request request) async {
print('Hello received'); print('Hello received');
@@ -151,11 +186,71 @@ void main() async {
headers: {'Access-Control-Allow-Origin': '*'}, headers: {'Access-Control-Allow-Origin': '*'},
); );
}); });
app.get('/fbla-api/businessdata/overview', (Request request) async { app.get('/fbla-api/businessdata/overview/jobs', (Request request) async {
print('business overview request received');
var typeFilters = request.url.queryParameters['typeFilters']?.split(',') ??
JobType.values.asNameMap().keys;
var offerFilters =
request.url.queryParameters['offerFilters']?.split(',') ??
OfferType.values.asNameMap().keys;
var postgresResult = (await postgres.query('''
SELECT jsonb_agg(
jsonb_build_object(
'id', b.id,
'name', b.name,
'contactName', b."contactName",
'contactEmail', b."contactEmail",
'contactPhone', b."contactPhone",
'locationName', b."locationName",
'locationAddress', b."locationAddress",
'listings', b.listings
)
) AS result
FROM (
SELECT
businesses.id,
businesses.name,
businesses."contactName",
businesses."contactEmail",
businesses."contactPhone",
businesses."locationName",
businesses."locationAddress",
jsonb_agg(
jsonb_build_object(
'id', listings.id,
'name', listings.name,
'description', listings.description,
'type', listings.type,
'wage', listings.wage,
'link', listings.link,
'offerType', listings."offerType"
)
) AS listings
FROM businesses
JOIN listings ON businesses.id = listings."businessId"
AND listings.type IN (${typeFilters.map((element) => "'$element'").join(',')})
AND listings."offerType" IN (${offerFilters.map((element) => "'$element'").join(',')})
GROUP BY businesses.id, businesses.name, businesses."contactName", businesses."contactEmail",
businesses."contactPhone", businesses."locationName", businesses."locationAddress"
) b
WHERE b.listings IS NOT NULL;
'''));
return Response.ok(
json.encode(postgresResult[0][0]),
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/plain'
},
);
});
app.get('/fbla-api/businessdata/overview/types', (Request request) async {
print('business overview request received'); print('business overview request received');
var filters = request.url.queryParameters['filters']?.split(',') ?? var filters = request.url.queryParameters['filters']?.split(',') ??
JobType.values.asNameMap().keys; BusinessType.values.asNameMap().keys;
// List<Map<String, List<Map<String, dynamic>>>> this is the real type lol // List<Map<String, List<Map<String, dynamic>>>> this is the real type lol
Map<String, dynamic> output = {}; Map<String, dynamic> output = {};
@@ -170,9 +265,10 @@ void main() async {
'website', website, 'website', website,
'contactEmail', "contactEmail", 'contactEmail', "contactEmail",
'contactPhone', "contactPhone", 'contactPhone', "contactPhone",
'locationName', "locationName" 'locationName', "locationName",
'locationAddress', "locationAddress"
) )
) FROM public.businesses WHERE id IN (SELECT "businessId" FROM public.listings WHERE type='${filters.elementAt(i)}') ) FROM public.businesses WHERE type='${filters.elementAt(i)}'
'''))[0][0]; '''))[0][0];
if (postgresResult != null) { if (postgresResult != null) {
@@ -188,6 +284,26 @@ void main() async {
}, },
); );
}); });
app.get('/fbla-api/businessdata/businessnames', (Request request) async {
print('business names request received');
var postgresResult = (await postgres.query('''
SELECT json_agg(
json_build_object(
'id', id,
'name', name
)
) FROM public.businesses
'''))[0][0];
return Response.ok(
json.encode(postgresResult),
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/plain'
},
);
});
app.get('/fbla-api/businessdata/business/<business>', app.get('/fbla-api/businessdata/business/<business>',
(Request request, String business) async { (Request request, String business) async {
print('idividual business data request received'); print('idividual business data request received');
@@ -198,6 +314,7 @@ void main() async {
'id', b.id, 'id', b.id,
'name', b.name, 'name', b.name,
'description', b.description, 'description', b.description,
'type', b.type,
'website', b.website, 'website', b.website,
'contactName', b."contactName", 'contactName', b."contactName",
'contactEmail', b."contactEmail", 'contactEmail', b."contactEmail",
@@ -206,19 +323,24 @@ void main() async {
'locationName', b."locationName", 'locationName', b."locationName",
'locationAddress', b."locationAddress", 'locationAddress', b."locationAddress",
'listings', 'listings',
json_agg( CASE
WHEN COUNT(l.id) = 0 THEN 'null'
ELSE json_agg(
json_build_object( json_build_object(
'id', l.id, 'id', l.id,
'businessId', l."businessId",
'name', l.name, 'name', l.name,
'description', l.description, 'description', l.description,
'type', l.type, 'type', l.type,
'offerType', l."offerType",
'wage', l.wage, 'wage', l.wage,
'link', l.link 'link', l.link
) )
) )
END
) )
FROM businesses b FROM businesses b
LEFT JOIN listings l ON b.id = l.business_id LEFT JOIN listings l ON b.id = l."businessId"
WHERE b.id = $business WHERE b.id = $business
GROUP BY b.id; GROUP BY b.id;
'''))[0][0]; '''))[0][0];
@@ -231,11 +353,74 @@ void main() async {
}, },
); );
}); });
app.get('/fbla-api/businessdata/businesses', (Request request) async {
print('list of business data request received');
if (request.url.queryParameters['businesses'] == null) {
return Response.badRequest(
body: 'query \'businesses\' required',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/plain'
},
);
}
var filters = request.url.queryParameters['businesses']!.split(',');
var result = (await postgres.query('''
SELECT
json_build_object(
'id', b.id,
'name', b.name,
'description', b.description,
'website', b.website,
'type', b.type,
'contactName', b."contactName",
'contactEmail', b."contactEmail",
'contactPhone', b."contactPhone",
'notes', b.notes,
'locationName', b."locationName",
'locationAddress', b."locationAddress"
)
FROM businesses b
LEFT JOIN listings l ON b.id = l."businessId"
WHERE b.id IN (${filters.join(',')})
GROUP BY b.id;
'''));
var output = result.map((element) => element[0]).toList();
return Response.ok(
json.encode(output),
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/plain'
},
);
});
app.get('/fbla-api/businessdata', (Request request) async { app.get('/fbla-api/businessdata', (Request request) async {
print('business data request received'); print('business data request received');
final output = await fetchBusinessData(); final result = await postgres.query('''
SELECT json_agg(
json_build_object(
'id', id,
'name', name,
'description', description,
'type', type,
'website', website,
'contactName', "contactName",
'contactEmail', "contactEmail",
'contactPhone', "contactPhone",
'notes', notes,
'locationName', "locationName",
'locationAddress', "locationAddress"
)
) FROM businesses
''');
var encoded = json.encode(result[0][0]);
return Response.ok( return Response.ok(
output.toString(), encoded,
headers: { headers: {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
@@ -246,8 +431,8 @@ void main() async {
print('business logo request received'); print('business logo request received');
var logo = File('logos/$logoId.png'); var logo = File('logos/$logoId.png');
try {
List<int> content = logo.readAsBytesSync(); List<int> content = logo.readAsBytesSync();
return Response.ok( return Response.ok(
content, content,
headers: { headers: {
@@ -255,6 +440,16 @@ void main() async {
'Content-Type': 'image/png' 'Content-Type': 'image/png'
}, },
); );
} catch (e) {
print('Error reading logo!');
return Response.notFound(
'logo not found',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'image/png'
},
);
}
}); });
app.post('/fbla-api/createbusiness', (Request request) async { app.post('/fbla-api/createbusiness', (Request request) async {
print('create business request received'); print('create business request received');
@@ -267,9 +462,10 @@ void main() async {
Business business = Business.fromJson(json); Business business = Business.fromJson(json);
await postgres.query(''' await postgres.query('''
INSERT INTO businesses (name, description, website, "contactName", "contactPhone", "contactEmail", notes, "locationName", "locationAddress") INSERT INTO businesses (name, description, website, type, "contactName", "contactPhone", "contactEmail", notes, "locationName", "locationAddress")
VALUES ('${business.name.replaceAll("'", "''")}', '${business.description.replaceAll("'", "''")}', '${business.website ?? 'NULL'}', '${business.contactName?.replaceAll("'", "''") ?? 'NULL'}', '${business.contactPhone ?? 'NULL'}', '${business.contactEmail ?? 'NULL'}', '${business.notes?.replaceAll("'", "''") ?? 'NULL'}', '${business.locationName?.replaceAll("'", "''") ?? 'NULL'}', '${business.locationAddress?.replaceAll("'", "''") ?? 'NULL'}') VALUES ('${business.name.replaceAll("'", "''")}', '${business.description.replaceAll("'", "''")}', '${business.website ?? 'NULL'}', '${business.type?.name}', '${business.contactName?.replaceAll("'", "''") ?? 'NULL'}', '${business.contactPhone ?? 'NULL'}', '${business.contactEmail ?? 'NULL'}', '${business.notes?.replaceAll("'", "''") ?? 'NULL'}', '${business.locationName?.replaceAll("'", "''") ?? 'NULL'}', '${business.locationAddress?.replaceAll("'", "''") ?? 'NULL'}')
'''); '''
.replaceAll("'null'", 'NULL'));
final dbBusiness = await postgres.query('''SELECT * FROM public.businesses final dbBusiness = await postgres.query('''SELECT * FROM public.businesses
ORDER BY id DESC LIMIT 1'''); ORDER BY id DESC LIMIT 1''');
@@ -307,9 +503,10 @@ void main() async {
JobListing listing = JobListing.fromJson(json); JobListing listing = JobListing.fromJson(json);
await postgres.query(''' await postgres.query('''
INSERT INTO listings ("businessId", name, description, type, wage, link) INSERT INTO listings ("businessId", name, description, type, wage, link, "offerType")
VALUES ('${listing.businessId}' '${listing.name.replaceAll("'", "''")}', '${listing.description.replaceAll("'", "''")}', '${listing.type.name}', '${listing.wage ?? 'NULL'}', '${listing.link?.replaceAll("'", "''") ?? 'NULL'}') VALUES ('${listing.businessId}', '${listing.name.replaceAll("'", "''")}', '${listing.description.replaceAll("'", "''")}', '${listing.type.name}', '${listing.wage ?? 'NULL'}', '${listing.link?.replaceAll("'", "''") ?? 'NULL'}', '${listing.offerType.name}')
'''); '''
.replaceAll("'null'", 'NULL'));
final dbListing = await postgres.query('''SELECT id FROM public.listings final dbListing = await postgres.query('''SELECT id FROM public.listings
ORDER BY id DESC LIMIT 1'''); ORDER BY id DESC LIMIT 1''');
@@ -405,7 +602,8 @@ void main() async {
UPDATE businesses SET UPDATE businesses SET
name = '${business.name.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, description = '${business.description.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, website = '${business.website!}'::text, "contactName" = '${business.contactName!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "contactPhone" = '${business.contactPhone!}'::text, "contactEmail" = '${business.contactEmail!}'::text, notes = '${business.notes!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "locationName" = '${business.locationName!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "locationAddress" = '${business.locationAddress!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text WHERE name = '${business.name.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, description = '${business.description.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, website = '${business.website!}'::text, "contactName" = '${business.contactName!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "contactPhone" = '${business.contactPhone!}'::text, "contactEmail" = '${business.contactEmail!}'::text, notes = '${business.notes!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "locationName" = '${business.locationName!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "locationAddress" = '${business.locationAddress!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text WHERE
id = ${business.id}; id = ${business.id};
'''); '''
.replaceAll("'null'", 'NULL'));
var logoResponse = await http.get( var logoResponse = await http.get(
Uri.http('logo.clearbit.com', '/${business.website}'), Uri.http('logo.clearbit.com', '/${business.website}'),
@@ -449,9 +647,10 @@ void main() async {
await postgres.query(''' await postgres.query('''
UPDATE listings SET UPDATE listings SET
"businessId" = ${listing.businessId}, name = '${listing.name.replaceAll("'", "''")}'::text, description = '${listing.description.replaceAll("'", "''")}'::text, type = '${listing.type.name}'::text, wage = '${listing.wage ?? 'NULL'}'::text, link = '${listing.link?.replaceAll("'", "''") ?? 'NULL'}'::text WHERE "businessId" = ${listing.businessId}, name = '${listing.name.replaceAll("'", "''")}'::text, description = '${listing.description.replaceAll("'", "''")}'::text, type = '${listing.type.name}'::text, wage = '${listing.wage ?? 'NULL'}'::text, link = '${listing.link?.replaceAll("'", "''") ?? 'NULL'}'::text, "offerType"='${listing.offerType.name}'::text WHERE
id = ${listing.id}; id = ${listing.id};
'''); '''
.replaceAll("'null'", 'NULL'));
return Response.ok( return Response.ok(
listing.id.toString(), listing.id.toString(),
@@ -630,6 +829,31 @@ void main() async {
}, },
); );
}); });
app.get('/fbla-api/clearbit/<website>',
(Request request, String website) async {
print('clearbit logo request received');
website = Uri.decodeComponent(website);
var response =
await http.get(Uri.parse('https://logo.clearbit.com/$website'));
if (response.statusCode == 200) {
return Response.ok(
response.bodyBytes,
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'image/png'
},
);
} else {
return Response.notFound(
'logo not found',
headers: {
'Access-Control-Allow-Origin': '*',
},
);
}
});
// get ip address for hosting // get ip address for hosting
for (var interface in await NetworkInterface.list()) { for (var interface in await NetworkInterface.list()) {
+1 -1
View File
@@ -3,7 +3,7 @@ pipeline {
stages { stages {
stage('Flutter Cleanup') { stage('Flutter Cleanup') {
steps { steps {
sh '''flutter upgrade --force sh '''flutter upgrade
flutter pub upgrade flutter pub upgrade
flutter --version flutter --version
flutter doctor flutter doctor
+3
View File
@@ -7,6 +7,9 @@
# The following line activates a set of recommended lints for Flutter apps, # The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices. # packages, and plugins designed to encourage good coding practices.
analyzer:
errors:
use_build_context_synchronously: ignore
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
linter: linter:
+6 -10
View File
@@ -1,3 +1,9 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties() def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties') def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) { if (localPropertiesFile.exists()) {
@@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
} }
} }
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
flutterVersionCode = '1' flutterVersionCode = '1'
@@ -21,10 +22,6 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
namespace "com.marinodev.fbla_ui" namespace "com.marinodev.fbla_ui"
compileSdkVersion flutter.compileSdkVersion compileSdkVersion flutter.compileSdkVersion
@@ -69,5 +66,4 @@ flutter {
} }
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
} }
+1 -13
View File
@@ -1,16 +1,3 @@
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects { allprojects {
repositories { repositories {
google() google()
@@ -22,6 +9,7 @@ rootProject.buildDir = '../build'
subprojects { subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}" project.buildDir = "${rootProject.buildDir}/${project.name}"
} }
subprojects { subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }
+22 -8
View File
@@ -1,11 +1,25 @@
include ':app' pluginManagement {
def flutterSdkPath = {
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties() def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk") def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties" assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.9.24" apply false
}
include ":app"
+63
View File
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="200mm"
height="70mm"
viewBox="0 0 200 70"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
sodipodi:docname="MDEVLogoFull.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.75544332"
inkscape:cx="47.654138"
inkscape:cy="97.955728"
inkscape:window-width="1920"
inkscape:window-height="1046"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs1"><rect
x="164.6723"
y="176.95522"
width="237.7821"
height="119.97466"
id="rect1047" /><rect
x="170.3439"
y="136.68353"
width="277.49832"
height="196.87138"
id="rect9139" /></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><path
style="font-size:48px;font-family:HeadlineNEWS;-inkscape-font-specification:HeadlineNEWS;white-space:pre;shape-padding:0.133618;fill:#000000;fill-opacity:1;stroke-width:0.608723"
d="m 103.58306,22.62203 h -7.380754 q 0,-2.975635 0,-5.95127 0,-4.113378 -0.233384,-5.74706 l -4.113377,8.489312 -3.617439,-0.08752 -3.82165,-8.401793 q -0.262556,1.779547 -0.291729,5.74706 0,2.975635 0,5.95127 h -7.380745 l 1.10857,-21.90884035 h 7.205708 l 5.105257,9.91878235 5.134429,-9.91878235 h 7.147374 z m 22.60912,0 h -5.68875 l -0.90436,-2.683906 h -7.20575 l -0.93354,2.683906 h -5.71792 L 112.7434,5.1182952 h 6.47642 z m -7.52665,-5.54285 -2.71309,-8.4601379 -2.65475,8.4601379 z m 27.62688,5.54285 h -6.12634 q -1.72121,-3.442402 -1.80873,-3.617439 -1.40031,-2.479696 -2.45054,-3.033981 l -1.72121,-0.904359 V 22.62203 H 128.4385 V 5.1182952 h 9.71463 q 2.77144,0 4.43431,1.050224 2.01294,1.2836072 2.01294,3.8799948 0,1.808719 -1.16692,2.888116 -0.81685,0.787668 -2.77145,1.51699 1.25445,0.583458 3.23821,3.850822 1.1961,2.158794 2.39219,4.317588 z m -7.29326,-12.194269 q 0,-2.4505225 -3.034,-2.4505225 h -1.77956 V 12.93663 h 1.77956 q 1.31279,0 2.15881,-0.612631 0.87519,-0.641804 0.87519,-1.896238 z m 15.66591,12.194269 h -6.068 V 5.1182952 h 6.068 z m 22.40489,0 h -5.33867 l -7.67251,-9.743746 0.20421,9.743746 h -5.74709 V 5.1182952 h 5.36784 l 7.76003,9.6562268 -0.26256,-9.6562268 h 5.68875 z m 22.02554,-8.839386 q 0,4.230069 -2.59639,6.709764 -2.59639,2.450523 -6.85563,2.450523 -4.23007,0 -6.85563,-2.42135 -2.62556,-2.42135 -2.62556,-6.622246 0,-4.1717228 2.71308,-6.6805917 2.62556,-2.4505228 6.85563,-2.4505228 4.11338,0 6.70976,2.4213499 2.65474,2.5088688 2.65474,6.5930736 z m -5.74706,0.02917 q 0,-1.808719 -0.96271,-2.946462 -0.9627,-1.1669148 -2.74225,-1.1669148 -1.83789,0 -2.77143,1.2252608 -0.87518,1.10857 -0.87518,2.975635 0,1.837892 0.87518,2.917289 0.93354,1.196089 2.71308,1.196089 3.76331,0 3.76331,-4.200897 z"
id="text9137"
inkscape:label="Marino"
aria-label="Marino" /><path
d="m 104.7848,29.612184 q 8.86861,0 14.46983,5.4456 5.60122,5.393746 5.60122,14.210437 0,8.505515 -5.18631,13.899254 -5.13446,5.341879 -13.58816,5.341879 H 86.114135 v -38.89717 z m 6.37918,19.500449 q 0,-4.200897 -2.1264,-6.690317 -2.28198,-2.696869 -6.43104,-2.696869 h -2.956187 v 18.774366 h 3.059917 q 4.25279,0 6.43104,-2.645003 2.02267,-2.437559 2.02267,-6.742177 z m 45.19311,19.396721 H 128.71401 V 29.560317 h 27.53935 v 8.92042 H 142.2503 v 5.964233 h 12.65462 v 8.868553 H 142.2503 v 6.327278 h 14.10679 z m 43.5333,-38.949037 -15.40328,38.949037 H 173.18098 L 157.31084,29.560317 h 14.41797 l 7.62387,23.234579 q 0.15559,-0.20745 7.31267,-23.234579 z"
id="text1045"
style="font-size:85.3333px;font-family:HeadlineNEWS;-inkscape-font-specification:HeadlineNEWS;letter-spacing:-4.66px;white-space:pre;fill:#000000;fill-opacity:1;stroke-width:0.608723"
inkscape:label="Dev"
aria-label="DEV" /><path
d="m 39.967467,0 c -1.24713,0 -2.07854,1.440059 -2.07854,1.440059 l -4.57281,7.920321 17.81611,30.858404 c 4.37185,-0.99615 6.86321,4.80385 3.14145,7.29302 -3.72178,2.48917 -8.13084,-2.02826 -5.54068,-5.68838 v 0 L 31.653287,12.240495 0.47509223,66.242704 c -0.55431396,0.96004 -0.62353496,1.80008 -0.20783896,2.52011 0.41568996,0.72005 1.17784883,1.08004 2.28640583,1.08004 h 9.1456049 l 17.816103,-30.8584 c -3.04862,-3.28806 0.72867,-8.34565 4.74522,-6.36708 4.01656,1.97857 2.30888,8.05564 -2.15596,7.64256 v 0 l -17.079704,29.58292 h 62.356384 c 0,0 1.66283,0 2.2864,-1.08004 0.62353,-1.08005 -0.20784,-2.52011 -0.20784,-2.52011 l -4.57281,-7.92032 h -35.63222 c -1.32322,4.28422 -7.59186,3.5418 -7.88666,-0.92594 -0.29484,-4.46773 5.82195,-6.02738 7.69662,-1.95418 v 0 h 34.15941 L 42.046017,1.440059 c 0,0 -0.83138,-1.440059 -2.07855,-1.440059"
style="display:inline;fill:#2096f3;fill-opacity:1;stroke:none;stroke-width:2.37548"
id="path395"
inkscape:label="triangle"
inkscape:transform-center-y="-11.666667" /></g></svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

-206
View File
@@ -1,206 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:fbla_ui/shared.dart';
import 'package:http/http.dart' as http;
var apiAddress = 'https://homelab.marinodev.com/fbla-api';
var client = http.Client();
// var apiAddress = '192.168.0.114:8000';
Future fetchBusinessData() async {
try {
var response = await http
.get(Uri.parse('$apiAddress/businessdata'))
.timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
var decodedResponse = json.decode(response.body);
List<Business> businessList = List<Business>.from(
decodedResponse.map((json) => Business.fromJson(json)).toList());
return businessList;
} else {
return 'Error ${response.statusCode}! Please try again later!';
}
} on TimeoutException {
return 'Unable to connect to server (timeout).\nPlease try again later.';
} on SocketException {
return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n';
}
}
Future fetchBusinessDataOverview() async {
try {
var response = await http
.get(Uri.parse('$apiAddress/businessdata/overview'))
.timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
var decodedResponse = json.decode(response.body);
Map<JobType, List<Business>> groupedBusinesses = {};
for (String stringType in decodedResponse.keys) {
groupedBusinesses.addAll({
JobType.values.byName(decodedResponse[stringType]):
decodedResponse.map((json) => Business.fromJson(json)).toList()
});
}
// for (JobType type in decodedResponse.keys) {
// groupedBusinesses.addAll({
// JobType.values.byName(decodedResponse[i]):
// decodedResponse.map((json) => Business.fromJson(json)).toList()
// });
// }
print(groupedBusinesses);
return groupedBusinesses;
} else {
return 'Error ${response.statusCode}! Please try again later!';
}
} on TimeoutException {
return 'Unable to connect to server (timeout).\nPlease try again later.';
} on SocketException {
return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n';
}
}
Future fetchBusiness(int id) async {
try {
var response = await http
.get(Uri.parse('$apiAddress/businessdata/business/$id'))
.timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
var decodedResponse = json.decode(response.body);
Business business = Business.fromJson(decodedResponse);
return business;
} else {
return 'Error ${response.statusCode}! Please try again later!';
}
} on TimeoutException {
return 'Unable to connect to server (timeout).\nPlease try again later.';
} on SocketException {
return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n';
}
}
Future createBusiness(Business business, String jwt) async {
var json = '''
{
"id": ${business.id},
"name": "${business.name}",
"description": "${business.description}",
"website": "${business.website}",
"contactName": "${business.contactName}",
"contactEmail": "${business.contactEmail}",
"contactPhone": "${business.contactPhone}",
"notes": "${business.notes}",
"locationName": "${business.locationName}",
"locationAddress": "${business.locationAddress}"
}
''';
try {
var response = await http.post(Uri.parse('$apiAddress/createbusiness'),
body: json,
headers: {'Authorization': jwt}).timeout(const Duration(seconds: 20));
if (response.statusCode != 200) {
return response.body;
}
} on TimeoutException {
return 'Unable to connect to server (timeout). Please try again later';
} on SocketException {
return 'Unable to connect to server (socket exception). Please check your internet connection.';
}
}
Future deleteBusiness(int id, String jwt) async {
var json = '''
{
"id": $id
}
''';
try {
var response = await http.post(Uri.parse('$apiAddress/deletebusiness'),
body: json,
headers: {'Authorization': jwt}).timeout(const Duration(seconds: 20));
if (response.statusCode != 200) {
return response.body;
}
} on TimeoutException {
return 'Unable to connect to server (timeout). Please try again later';
} on SocketException {
return 'Unable to connect to server (socket exception). Please check your internet connection.';
}
}
Future editBusiness(Business business, String jwt) async {
var json = '''
{
"id": ${business.id},
"name": "${business.name}",
"description": "${business.description}",
"website": "${business.website}",
"contactName": "${business.contactName}",
"contactEmail": "${business.contactEmail}",
"contactPhone": "${business.contactPhone}",
"notes": "${business.notes}",
"locationName": "${business.locationName}",
"locationAddress": "${business.locationAddress}"
}
''';
try {
var response = await http.post(Uri.parse('$apiAddress/editbusiness'),
body: json,
headers: {'Authorization': jwt}).timeout(const Duration(seconds: 20));
if (response.statusCode != 200) {
return response.body;
}
} on TimeoutException {
return 'Unable to connect to server (timeout). Please try again later';
} on SocketException {
return 'Unable to connect to server (socket exception). Please check your internet connection.';
}
}
Future signIn(String username, String password) async {
var json = '''
{
"username": "$username",
"password": "$password"
}
''';
var response = await http.post(
Uri.parse('$apiAddress/signin'),
body: json,
);
if (response.statusCode == 200) {
return response.body;
} else {
return 'Error: ${response.body}';
}
}
Future marinoDevLogo() async {
var response = await http.get(
Uri.parse('$apiAddress/marinodev'),
);
// File logo = File ('${getTemporaryDirectory().toString()}/marinodev.svg');
// logo.writeAsBytes(response.bodyBytes);
return response.bodyBytes;
}
Future getLogo(int logoId) async {
var response = await http.get(
Uri.parse('$apiAddress/logos/$logoId'),
);
if (response.statusCode == 200) {
return response.bodyBytes;
} else {
return 'Error ${response.statusCode}';
}
}
+239 -379
View File
@@ -1,37 +1,51 @@
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:fbla_ui/pages/businesses_overview.dart';
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/main.dart';
import 'package:fbla_ui/pages/create_edit_business.dart'; import 'package:fbla_ui/pages/create_edit_business.dart';
import 'package:fbla_ui/pages/export_data.dart'; import 'package:fbla_ui/pages/create_edit_listing.dart';
import 'package:fbla_ui/pages/signin_page.dart'; import 'package:fbla_ui/pages/listings_overview.dart';
import 'package:fbla_ui/shared.dart'; import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
typedef Callback = void Function();
class Home extends StatefulWidget { class Home extends StatefulWidget {
final Callback themeCallback; final void Function() themeCallback;
final int? initialPage;
const Home({super.key, required this.themeCallback}); const Home({super.key, required this.themeCallback, this.initialPage});
@override @override
State<Home> createState() => _HomeState(); State<Home> createState() => _HomeState();
} }
class _HomeState extends State<Home> { class _HomeState extends State<Home> {
late Future refreshBusinessDataOverviewFuture; Set<JobType> jobTypeFilters = <JobType>{};
bool _isPreviousData = false; Set<OfferType> offerTypeFilters = <OfferType>{};
late Map<JobType, List<Business>> overviewBusinesses; Set<BusinessType> businessTypeFilters = <BusinessType>{};
String searchQuery = '';
late Future refreshBusinessDataOverviewJobFuture;
late Future refreshBusinessDataOverviewBusinessFuture;
int currentPageIndex = 0;
late dynamic previousJobData;
ScrollController scrollControllerBusinesses = ScrollController();
ScrollController scrollControllerJobs = ScrollController();
void _updateLoggedIn(bool updated) {
setState(() {
loggedIn = updated;
});
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
refreshBusinessDataOverviewFuture = fetchBusinessDataOverview(); currentPageIndex = widget.initialPage ?? 0;
initialLogin(); initialLogin();
refreshBusinessDataOverviewJobFuture = fetchBusinessDataOverviewJobs();
refreshBusinessDataOverviewBusinessFuture =
fetchBusinessDataOverviewTypes();
} }
Future<void> initialLogin() async { Future<void> initialLogin() async {
@@ -51,401 +65,247 @@ class _HomeState extends State<Home> {
} }
} }
void setStateCallback() { Future<void> _updateOverviewBusinessesJobsCallback(
Set<JobType>? newJobTypeFilters,
Set<OfferType>? newOfferTypeFilters) async {
if (newJobTypeFilters != null) {
jobTypeFilters = Set.from(newJobTypeFilters);
}
if (newOfferTypeFilters != null) {
offerTypeFilters = Set.from(newOfferTypeFilters);
}
var refreshedData = fetchBusinessDataOverviewJobs(
typeFilters: jobTypeFilters, offerFilters: offerTypeFilters);
await refreshedData;
setState(() { setState(() {
loggedIn = loggedIn; refreshBusinessDataOverviewJobFuture = refreshedData;
});
}
Future<void> _updateOverviewBusinessesBusinessCallback(
Set<BusinessType>? newFilters) async {
if (newFilters != null) {
businessTypeFilters = Set.from(newFilters);
}
var refreshedData = fetchBusinessDataOverviewTypes(
typeFilters: businessTypeFilters.toList());
await refreshedData;
setState(() {
refreshBusinessDataOverviewBusinessFuture = refreshedData;
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool widescreen = MediaQuery.sizeOf(context).width >= 1000; bool widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth;
return Scaffold( return Scaffold(
floatingActionButton: _getFAB(), bottomNavigationBar: _getNavigationBar(widescreen),
body: RefreshIndicator( body: RefreshIndicator(
edgeOffset: 120, edgeOffset: 145,
onRefresh: () async { onRefresh: () async {
var refreshedData = fetchBusinessDataOverview(); _updateOverviewBusinessesJobsCallback(null, null);
await refreshedData; _updateOverviewBusinessesBusinessCallback(null);
},
child: widescreen
? Row(
children: [
_getNavigationRail(),
Expanded(
child: _ContentPane(
themeCallback: widget.themeCallback,
searchQuery: searchQuery,
currentPageIndex: currentPageIndex,
refreshBusinessDataOverviewBusinessFuture:
refreshBusinessDataOverviewBusinessFuture,
refreshBusinessDataOverviewJobFuture:
refreshBusinessDataOverviewJobFuture,
updateOverviewBusinessesBusinessCallback:
_updateOverviewBusinessesBusinessCallback,
updateOverviewBusinessesJobsCallback:
_updateOverviewBusinessesJobsCallback,
updateLoggedIn: _updateLoggedIn,
),
)
],
)
: _ContentPane(
themeCallback: widget.themeCallback,
searchQuery: searchQuery,
currentPageIndex: currentPageIndex,
refreshBusinessDataOverviewBusinessFuture:
refreshBusinessDataOverviewBusinessFuture,
refreshBusinessDataOverviewJobFuture:
refreshBusinessDataOverviewJobFuture,
updateOverviewBusinessesBusinessCallback:
_updateOverviewBusinessesBusinessCallback,
updateOverviewBusinessesJobsCallback:
_updateOverviewBusinessesJobsCallback,
updateLoggedIn: _updateLoggedIn,
),
),
);
}
Widget? _getNavigationBar(bool widescreen) {
if (!widescreen) {
return NavigationBar(
selectedIndex: currentPageIndex,
indicatorColor: Theme.of(context).colorScheme.primary.withOpacity(0.5),
onDestinationSelected: (int index) {
setState(() { setState(() {
refreshBusinessDataOverviewFuture = refreshedData; currentPageIndex = index;
}); });
}, },
child: CustomScrollView( destinations: <NavigationDestination>[
slivers: [ NavigationDestination(
SliverAppBar( icon: const Icon(Icons.business_outlined),
title: widescreen ? _searchBar() : const Text('Job Link'), selectedIcon: Icon(
toolbarHeight: 70, Icons.business,
pinned: true, color: Theme.of(context).colorScheme.onSurface,
scrolledUnderElevation: 0, ),
centerTitle: true, label: 'Businesses'),
expandedHeight: widescreen ? 70 : 120, NavigationDestination(
backgroundColor: Theme.of(context).colorScheme.surface, icon: const Icon(Icons.work_outline),
bottom: _getBottom(), selectedIcon: Icon(
leading: IconButton( Icons.work,
icon: getIconFromThemeMode(themeMode), color: Theme.of(context).colorScheme.onSurface,
),
label: 'Job Listings'),
],
);
}
return null;
}
Widget _getNavigationRail() {
return Row(
children: [
NavigationRail(
selectedIndex: currentPageIndex,
indicatorColor:
Theme.of(context).colorScheme.primary.withOpacity(0.5),
trailing: Expanded(
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.all(16),
child: IconButton(
iconSize: 30,
icon: Icon(
getIconFromThemeMode(themeMode),
),
onPressed: () { onPressed: () {
setState(() { setState(() {
widget.themeCallback(); widget.themeCallback();
}); });
}, },
), ),
actions: [ ),
IconButton( ),
icon: const Icon(Icons.help), ),
onPressed: () { leading: Column(
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('About'),
backgroundColor:
Theme.of(context).colorScheme.background,
content: SizedBox(
width: 500,
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text(
'Welcome to my FBLA 2024 Coding and Programming submission!\n\n'
'MarinoDev Job Link aims to provide comprehensive details of businesses and community partners'
' for Waukesha West High School\'s Career and Technical Education Department.\n\n'),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
child: const Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text('Git Repo:'),
Text(
'https://git.marinodev.com/MarinoDev/FBLA24\n',
style: TextStyle(
color: Colors.blue)),
],
),
onTap: () {
launchUrl(Uri.https(
'git.marinodev.com',
'/MarinoDev/FBLA24'));
},
),
),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
child: const Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'Please direct any questions to'),
Text('drake@marinodev.com',
style: TextStyle(
color: Colors.blue)),
],
),
onTap: () {
launchUrl(Uri.parse(
'mailto:drake@marinodev.com'));
},
),
)
],
),
),
),
actions: [
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
}),
],
);
});
},
),
IconButton(
icon: const Icon(Icons.picture_as_pdf),
onPressed: () async {
if (!_isPreviousData) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text('There is no data!'),
duration: Duration(seconds: 2),
),
);
} else {
selectedDataTypes = <DataType>{};
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ExportData(
groupedBusinesses: overviewBusinesses)));
}
},
),
Padding( Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(top: 2.0, bottom: 8.0),
child: IconButton( child: Image.asset(
icon: loggedIn 'assets/Triangle256.png',
? const Icon(Icons.account_circle)
: const Icon(Icons.login),
onPressed: () {
if (loggedIn) {
var payload = JWT.decode(jwt).payload;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.background,
title: Text('Hi, ${payload['username']}!'),
content: Text(
'You are logged in as an admin with username ${payload['username']}.'),
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Logout'),
onPressed: () async {
final prefs = await SharedPreferences
.getInstance();
prefs.setBool('rememberMe', false);
prefs.setString('username', '');
prefs.setString('password', '');
setState(() {
loggedIn = false;
});
Navigator.of(context).pop();
}),
],
);
});
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SignInPage(
refreshAccount: setStateCallback)));
}
},
),
),
],
),
FutureBuilder(
future: refreshBusinessDataOverviewFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
if (snapshot.data.runtimeType == String) {
_isPreviousData = false;
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
Center(
child: Text(snapshot.data,
textAlign: TextAlign.center)),
Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Text('Retry'),
onPressed: () {
var refreshedData =
fetchBusinessDataOverview();
setState(() {
refreshBusinessDataOverviewFuture =
refreshedData;
});
},
),
),
]),
));
}
overviewBusinesses = snapshot.data;
_isPreviousData = true;
return BusinessDisplayPanel(
groupedBusinesses: overviewBusinesses,
widescreen: widescreen,
selectable: false);
} else if (snapshot.hasError) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Text(
'Error when loading data! Error: ${snapshot.error}'),
));
}
} else if (snapshot.connectionState ==
ConnectionState.waiting) {
if (_isPreviousData) {
return BusinessDisplayPanel(
groupedBusinesses: overviewBusinesses,
widescreen: widescreen,
selectable: false);
} else {
return SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
// child: const CircularProgressIndicator(),
child: const SizedBox(
width: 75,
height: 75,
child: RiveAnimation.asset(
'assets/mdev_triangle_loading.riv'),
// child: RiveAnimation.file(
// 'assets/mdev_triangle_loading.riv',
// ),
),
));
}
}
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'\nError: ${snapshot.error}',
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
),
);
}),
const SliverToBoxAdapter(
child: SizedBox(
height: 80,
),
)
],
),
),
);
}
Widget? _getFAB() {
if (loggedIn) {
return FloatingActionButton(
child: const Icon(Icons.add_business),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CreateEditBusiness()));
},
);
}
return null;
}
Widget _searchBar() {
return SizedBox(
width: 800,
height: 50, height: 50,
child: TextField( ),
onChanged: (query) { ),
if (loggedIn)
FloatingActionButton(
heroTag: 'Homepage',
onPressed: () {
if (currentPageIndex == 0) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const CreateEditBusiness()));
} else if (currentPageIndex == 1) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const CreateEditJobListing()));
}
},
child: const Icon(Icons.add),
)
],
),
onDestinationSelected: (int index) {
setState(() { setState(() {
searchFilter = query; currentPageIndex = index;
}); });
}, },
decoration: InputDecoration( labelType: NavigationRailLabelType.all,
labelText: 'Search', destinations: <NavigationRailDestination>[
hintText: 'Search', NavigationRailDestination(
prefixIcon: const Padding( icon: const Icon(Icons.business_outlined),
padding: EdgeInsets.only(left: 8.0), selectedIcon: Icon(
child: Icon(Icons.search), Icons.business,
color: Theme.of(context).colorScheme.onSurface,
), ),
border: const OutlineInputBorder( label: const Text('Businesses')),
borderRadius: BorderRadius.all(Radius.circular(25.0)), NavigationRailDestination(
icon: const Icon(Icons.work_outline),
selectedIcon: Icon(
Icons.work,
color: Theme.of(context).colorScheme.onSurface,
),
label: const Text('Job Listings')),
],
), ),
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
tooltip: 'Filters',
icon: Icon(Icons.filter_list,
color: isFiltered
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onBackground),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
// DO NOT MOVE TO SEPARATE WIDGET, setState is needed in main tree
backgroundColor:
Theme.of(context).colorScheme.background,
title: const Text('Filter Options'),
content: const FilterChips(),
actions: [
TextButton(
child: const Text('Reset'),
onPressed: () {
setState(() {
filters = <BusinessType>{};
selectedChips = <BusinessType>{};
isFiltered = false;
});
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Cancel'),
onPressed: () {
selectedChips = Set.from(filters);
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Apply'),
onPressed: () {
setState(() {
filters = Set.from(selectedChips);
if (filters.isNotEmpty) {
isFiltered = true;
} else {
isFiltered = false;
}
});
Navigator.of(context).pop();
}),
], ],
); );
}); }
},
),
),
),
),
);
} }
PreferredSizeWidget? _getBottom() { class _ContentPane extends StatelessWidget {
if (MediaQuery.sizeOf(context).width <= 1000) { final String searchQuery;
return PreferredSize( final Future refreshBusinessDataOverviewBusinessFuture;
preferredSize: const Size.fromHeight(0), final Future<void> Function(Set<BusinessType>)
child: SizedBox( updateOverviewBusinessesBusinessCallback;
// color: Theme.of(context).colorScheme.background, final void Function() themeCallback;
height: 70, final Future refreshBusinessDataOverviewJobFuture;
child: Padding( final Future<void> Function(Set<JobType>?, Set<OfferType>?)
padding: const EdgeInsets.all(10), updateOverviewBusinessesJobsCallback;
child: _searchBar(), final int currentPageIndex;
final void Function(bool) updateLoggedIn;
const _ContentPane({
required this.searchQuery,
required this.refreshBusinessDataOverviewBusinessFuture,
required this.updateOverviewBusinessesBusinessCallback,
required this.themeCallback,
required this.refreshBusinessDataOverviewJobFuture,
required this.updateOverviewBusinessesJobsCallback,
required this.currentPageIndex,
required this.updateLoggedIn,
});
@override
Widget build(BuildContext context) {
return IndexedStack(
index: currentPageIndex,
children: [
BusinessesOverview(
searchQuery: searchQuery,
refreshBusinessDataOverviewFuture:
refreshBusinessDataOverviewBusinessFuture,
updateBusinessesCallback: updateOverviewBusinessesBusinessCallback,
themeCallback: themeCallback,
updateLoggedIn: updateLoggedIn,
), ),
JobsOverview(
searchQuery: searchQuery,
refreshJobDataOverviewFuture: refreshBusinessDataOverviewJobFuture,
updateBusinessesCallback: updateOverviewBusinessesJobsCallback,
themeCallback: themeCallback,
updateLoggedIn: updateLoggedIn,
), ),
],
); );
} }
return null;
}
} }
+50 -14
View File
@@ -1,10 +1,9 @@
import 'package:fbla_ui/home.dart'; import 'package:fbla_ui/home.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
ThemeMode themeMode = ThemeMode.system;
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@@ -23,9 +22,9 @@ void main() async {
} }
class MainApp extends StatefulWidget { class MainApp extends StatefulWidget {
final bool? isDark; final int? initialPage;
const MainApp({super.key, this.isDark}); const MainApp({super.key, this.initialPage});
@override @override
State<MainApp> createState() => _MainAppState(); State<MainApp> createState() => _MainAppState();
@@ -69,35 +68,72 @@ class _MainAppState extends State<MainApp> {
return MaterialApp( return MaterialApp(
title: 'Job Link', title: 'Job Link',
themeMode: themeMode, themeMode: themeMode,
// themeMode: ThemeMode.light,
darkTheme: ThemeData( darkTheme: ThemeData(
scaffoldBackgroundColor: const Color(0xFF121212),
colorScheme: ColorScheme.dark( colorScheme: ColorScheme.dark(
brightness: Brightness.dark, brightness: Brightness.dark,
primary: Colors.blue, primary: Colors.blue.shade700,
onPrimary: Colors.white, onPrimary: Colors.white,
secondary: Colors.blue.shade900, secondary: Colors.blue.shade900,
background: const Color.fromARGB(255, 31, 31, 31), onSecondary: Colors.white,
surface: const Color.fromARGB(255, 31, 31, 31),
surfaceContainer: const Color.fromARGB(255, 46, 46, 46),
tertiary: Colors.green.shade900, tertiary: Colors.green.shade900,
), ),
iconTheme: const IconThemeData(color: Colors.white), iconTheme: const IconThemeData(color: Colors.white),
inputDecorationTheme: const InputDecorationTheme(),
useMaterial3: true, useMaterial3: true,
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.grey.withOpacity(0.1),
labelStyle: const TextStyle(color: Colors.grey),
floatingLabelStyle: WidgetStateTextStyle.resolveWith((states) {
if (states.contains(WidgetState.focused) &&
!states.contains(WidgetState.hovered)) {
return TextStyle(color: Colors.blue.shade700);
}
return const TextStyle(color: Colors.grey);
}),
),
dropdownMenuTheme: const DropdownMenuThemeData(
inputDecorationTheme: InputDecorationTheme(
filled: true,
),
),
), ),
theme: ThemeData( theme: ThemeData(
scaffoldBackgroundColor: Colors.grey.shade300,
colorScheme: ColorScheme.light( colorScheme: ColorScheme.light(
brightness: Brightness.light, brightness: Brightness.light,
primary: Colors.blue, primary: Colors.blue.shade700,
onPrimary: Colors.white, onPrimary: Colors.white,
secondary: Colors.blue.shade200, secondary: Colors.blue.shade300,
background: Colors.grey.shade300, onSecondary: Colors.black,
surface: Colors.grey.shade100,
surfaceContainer: Colors.grey.shade200,
tertiary: Colors.green, tertiary: Colors.green,
), ),
iconTheme: const IconThemeData(color: Colors.black), iconTheme: const IconThemeData(color: Colors.black),
inputDecorationTheme: inputDecorationTheme: InputDecorationTheme(
const InputDecorationTheme(border: UnderlineInputBorder()), // border: OutlineInputBorder(),
filled: true,
fillColor: Colors.grey.withOpacity(0.25),
labelStyle: TextStyle(color: Colors.grey.shade700),
floatingLabelStyle: WidgetStateTextStyle.resolveWith((states) {
if (states.contains(WidgetState.focused) &&
!states.contains(WidgetState.hovered)) {
return TextStyle(color: Colors.blue.shade700);
}
return TextStyle(color: Colors.grey.shade700);
}),
),
dropdownMenuTheme: const DropdownMenuThemeData(
inputDecorationTheme: InputDecorationTheme(
filled: true,
),
),
useMaterial3: true, useMaterial3: true,
), ),
home: Home(themeCallback: _switchTheme), home: Home(themeCallback: _switchTheme, initialPage: widget.initialPage),
); );
} }
} }
+197 -145
View File
@@ -1,8 +1,11 @@
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/main.dart'; import 'package:fbla_ui/main.dart';
import 'package:fbla_ui/pages/create_edit_business.dart'; import 'package:fbla_ui/pages/create_edit_business.dart';
import 'package:fbla_ui/pages/signin_page.dart'; import 'package:fbla_ui/pages/create_edit_listing.dart';
import 'package:fbla_ui/shared.dart'; import 'package:fbla_ui/pages/listing_detail.dart';
import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:fbla_ui/shared/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rive/rive.dart'; import 'package:rive/rive.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -10,13 +13,8 @@ import 'package:url_launcher/url_launcher.dart';
class BusinessDetail extends StatefulWidget { class BusinessDetail extends StatefulWidget {
final int id; final int id;
final String name; final String name;
final JobType clickFromType;
const BusinessDetail( const BusinessDetail({super.key, required this.id, required this.name});
{super.key,
required this.id,
required this.name,
required this.clickFromType});
@override @override
State<BusinessDetail> createState() => _CreateBusinessDetailState(); State<BusinessDetail> createState() => _CreateBusinessDetailState();
@@ -24,6 +22,7 @@ class BusinessDetail extends StatefulWidget {
class _CreateBusinessDetailState extends State<BusinessDetail> { class _CreateBusinessDetailState extends State<BusinessDetail> {
late Future loadBusiness; late Future loadBusiness;
bool _isRetrying = false;
@override @override
void initState() { void initState() {
@@ -43,7 +42,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(snapshot.data.name), title: Text(snapshot.data.name),
actions: _getActions(snapshot.data, widget.clickFromType), actions: _getActions(snapshot.data),
), ),
body: _detailBody(snapshot.data), body: _detailBody(snapshot.data),
); );
@@ -62,11 +61,17 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: FilledButton( child: FilledButton(
child: const Text('Retry'), child: const Text('Retry'),
onPressed: () { onPressed: () async {
var refreshedData = fetchBusiness(widget.id); if (!_isRetrying) {
setState(() {
_isRetrying = true;
});
var refreshedData =
await fetchBusiness(widget.id);
setState(() { setState(() {
loadBusiness = refreshedData; loadBusiness = refreshedData;
}); });
}
}, },
), ),
), ),
@@ -107,198 +112,142 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
}); });
} }
ListView _detailBody(Business business) { Widget _detailBody(Business business) {
return ListView( return ListView(
children: [ children: [
// Title, logo, desc, website // Title, logo, desc, website
Card( Center(
child: SizedBox(
width: 800,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(
children: [ children: [
ListTile( Padding(
title: Text(business.name, padding: const EdgeInsets.only(right: 8.0),
textAlign: TextAlign.left, child: ListTile(
titleAlignment: ListTileTitleAlignment.titleHeight,
title: Text(business.name!,
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold)), fontSize: 24, fontWeight: FontWeight.bold)),
subtitle: Text( subtitle: Text(
business.description, business.description!,
textAlign: TextAlign.left,
), ),
contentPadding:
const EdgeInsets.only(bottom: 8, left: 16),
leading: ClipRRect( leading: ClipRRect(
borderRadius: BorderRadius.circular(6.0), borderRadius: BorderRadius.circular(6.0),
child: Image.network('$apiAddress/logos/${business.id}', child: Image.network(
'$apiAddress/logos/${business.id}',
width: 48, width: 48,
height: 48, errorBuilder: (BuildContext context, height: 48, errorBuilder:
Object exception, StackTrace? stackTrace) { (BuildContext context, Object exception,
return getIconFromJobType(widget.clickFromType, 48, StackTrace? stackTrace) {
Theme.of(context).colorScheme.onSurface); return Icon(
getIconFromBusinessType(
business.type ?? BusinessType.other),
size: 48);
}), }),
), ),
), ),
),
if (business.website != null)
ListTile( ListTile(
leading: const Icon(Icons.link), leading: const Icon(Icons.link),
title: const Text('Website'), title: const Text('Website'),
subtitle: Text(business.website!, subtitle: Text(
business.website!
.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''),
style: const TextStyle(color: Colors.blue)), style: const TextStyle(color: Colors.blue)),
onTap: () { onTap: () {
launchUrl(Uri.parse('https://${business.website}')); launchUrl(Uri.parse(business.website!));
}, },
), ),
], ],
), ),
), ),
),
// Available positions // Available positions
if (business.listings != null || loggedIn)
Card( Card(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0),
child:
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text(
'Available Postitions',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
// Container(
// height: 400,
// width: 300,
ListView(
scrollDirection: Axis.vertical,
shrinkWrap: true,
children: [
ListTile(
title: Text('Postition 1'),
leading: Icon(Icons.work),
onTap: () {
// launchUrl(Uri.parse(''));
},
),
ListTile(
title: Text('Postition 2'),
leading: Icon(Icons.work),
onTap: () {
// launchUrl(Uri.parse(''));
},
),
ListTile(
title: Text('Postition 3'),
leading: Icon(Icons.work),
onTap: () {
// launchUrl(Uri.parse(''));
},
),
],
),
]),
),
),
// Contact info
Visibility(
visible:
(business.contactEmail != null || business.contactPhone != null),
child: Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Row(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0), padding: const EdgeInsets.only(left: 16, top: 8),
child: Text( child: _GetListingsTitle(business)),
business.contactName ?? 'Contact ${business.name}', if (business.listings != null)
textAlign: TextAlign.left, _JobList(business: business)
style: const TextStyle( else
fontSize: 20, fontWeight: FontWeight.bold), Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.only(left: 16.0),
child: Text('No job listings exist.'),
), ),
),
],
),
Visibility(
visible: business.contactPhone != null,
child: ListTile(
leading: Icon(Icons.phone),
title: Text(business.contactPhone!),
// maybe replace ! with ?? ''. same is true for below
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.background,
title: Text(business.contactName!.isEmpty
? 'Contact ${business.name}?'
: 'Contact ${business.contactName}'),
content: Text(business.contactName!.isEmpty
? 'Would you like to call or text ${business.name}?'
: 'Would you like to call or text ${business.contactName}?'),
actions: [
TextButton( TextButton(
child: const Text('Text'), child: const Text('add one?'),
onPressed: () { onPressed: () {
launchUrl(Uri.parse( Navigator.of(context).push(
'sms:${business.contactPhone}')); MaterialPageRoute(
Navigator.of(context).pop(); builder: (context) =>
}), CreateEditJobListing(
TextButton( inputBusiness: business,
child: const Text('Call'), )));
onPressed: () async {
launchUrl(Uri.parse(
'tel:${business.contactPhone}'));
Navigator.of(context).pop();
}),
],
);
});
}, },
), )
),
Visibility(
visible: business.contactEmail != null,
child: ListTile(
leading: const Icon(Icons.email),
title: Text(business.contactEmail!),
onTap: () {
launchUrl(Uri.parse('mailto:${business.contactEmail}'));
},
),
),
], ],
), ),
)
]),
), ),
), // Contact info
ContactInformationCard(business: business),
// Location // Location
Visibility( Card(
child: Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: ListTile( child: ListTile(
leading: const Icon(Icons.location_on), leading: const Icon(Icons.location_on),
title: Text(business.locationName!), title: Text(business.locationName),
subtitle: Text(business.locationAddress!), subtitle: Text(business.locationAddress!),
onTap: () { onTap: () {
launchUrl(Uri.parse(Uri.encodeFull( launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName}'))); 'https://www.google.com/maps/search/?api=1&query=${business.locationName} ${business.locationAddress}')));
}, },
), ),
), ),
),
// Notes // Notes
Visibility( if (business.notes != null && business.notes != '')
visible: business.notes != null, Card(
child: Card(
child: ListTile( child: ListTile(
leading: const Icon(Icons.notes), leading: const Icon(Icons.notes),
title: const Text( title: const Text(
'Additional Notes:', 'Additional Notes',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
), ),
subtitle: Text(business.notes!), subtitle: Text(business.notes!),
), ),
), ),
],
),
),
), ),
], ],
); );
} }
List<Widget>? _getActions(Business business, JobType clickFromType) { List<Widget>? _getActions(Business business) {
if (loggedIn) { if (loggedIn) {
return [ return [
IconButton( IconButton(
@@ -307,7 +256,6 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (context) => CreateEditBusiness( builder: (context) => CreateEditBusiness(
inputBusiness: business, inputBusiness: business,
clickFromType: clickFromType,
))); )));
}, },
), ),
@@ -318,7 +266,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.surface,
title: const Text('Are You Sure?'), title: const Text('Are You Sure?'),
content: content:
Text('This will permanently delete ${business.name}.'), Text('This will permanently delete ${business.name}.'),
@@ -332,7 +280,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
child: const Text('Yes'), child: const Text('Yes'),
onPressed: () async { onPressed: () async {
String? deleteResult = String? deleteResult =
await deleteBusiness(business.id, jwt); await deleteBusiness(business.id);
if (deleteResult != null) { if (deleteResult != null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@@ -356,3 +304,107 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
return null; return null;
} }
} }
class _JobList extends StatelessWidget {
final Business business;
const _JobList({required this.business});
@override
Widget build(BuildContext context) {
List<_JobListItem> listItems = [];
for (JobListing listing in business.listings!) {
listItems.add(_JobListItem(
jobListing: listing,
fromBusiness: business,
));
}
return ListView(
shrinkWrap: true,
children: listItems,
);
}
}
class _JobListItem extends StatelessWidget {
final JobListing jobListing;
final Business fromBusiness;
const _JobListItem({required this.jobListing, required this.fromBusiness});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(getIconFromJobType(jobListing.type!)),
title: Text(jobListing.name),
subtitle: Text(
jobListing.description,
style: const TextStyle(overflow: TextOverflow.ellipsis),
maxLines: 2,
),
trailing: _getEditIcon(context, fromBusiness, jobListing),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => JobListingDetail(
listing: jobListing,
fromBusiness: fromBusiness,
)));
},
);
}
Widget? _getEditIcon(
BuildContext context, Business fromBusiness, JobListing inputListing) {
if (loggedIn) {
return IconButton(
icon: const Icon(
Icons.edit,
),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => CreateEditJobListing(
inputBusiness: fromBusiness,
inputJobListing: inputListing,
)));
},
);
}
return null;
}
}
class _GetListingsTitle extends StatelessWidget {
final Business fromBusiness;
const _GetListingsTitle(this.fromBusiness);
@override
Widget build(BuildContext context) {
if (!loggedIn) {
return Text(
'Available Postitions (${fromBusiness.listings?.length ?? '0'})',
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold));
} else {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Available Postitions (${fromBusiness.listings?.length ?? '0'})',
style:
const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
Padding(
padding: const EdgeInsets.only(right: 24.0),
child: IconButton(
icon: const Icon(Icons.add),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => CreateEditJobListing(
inputBusiness: fromBusiness,
)));
},
),
)
],
);
}
}
}
+601
View File
@@ -0,0 +1,601 @@
import 'package:fbla_ui/pages/business_detail.dart';
import 'package:fbla_ui/pages/create_edit_business.dart';
import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/export.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:fbla_ui/shared/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:rive/rive.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:url_launcher/url_launcher.dart';
class BusinessesOverview extends StatefulWidget {
final String searchQuery;
final Future refreshBusinessDataOverviewFuture;
final Future<void> Function(Set<BusinessType>) updateBusinessesCallback;
final void Function() themeCallback;
final void Function(bool) updateLoggedIn;
const BusinessesOverview({
super.key,
required this.searchQuery,
required this.refreshBusinessDataOverviewFuture,
required this.updateBusinessesCallback,
required this.themeCallback,
required this.updateLoggedIn,
});
@override
State<BusinessesOverview> createState() => _BusinessesOverviewState();
}
class _BusinessesOverviewState extends State<BusinessesOverview> {
bool _isPreviousData = false;
late Map<BusinessType, List<Business>> overviewBusinesses;
Set<BusinessType> businessTypeFilters = <BusinessType>{};
String searchQuery = '';
ScrollController controller = ScrollController();
bool _extended = true;
double prevPixelPosition = 0;
bool _isRetrying = false;
Map<BusinessType, List<Business>> _filterBySearch(
Map<BusinessType, List<Business>> businesses, String query) {
Map<BusinessType, List<Business>> filteredBusinesses = {};
for (BusinessType businessType in businesses.keys) {
filteredBusinesses[businessType] = List.from(businesses[businessType]!
.where((element) => element.name!
.replaceAll(RegExp(r'[^a-zA-Z]'), '')
.toLowerCase()
.contains(query
.replaceAll(RegExp(r'[^a-zA-Z]'), '')
.toLowerCase()
.trim())));
}
filteredBusinesses.removeWhere((key, value) => value.isEmpty);
return filteredBusinesses;
}
void _setSearch(String search) async {
setState(() {
searchQuery = search;
});
}
void _setFilters(Set<BusinessType> filters) async {
businessTypeFilters = Set.from(filters);
widget.updateBusinessesCallback(businessTypeFilters);
}
void _scrollListener() {
if ((prevPixelPosition - controller.position.pixels).abs() > 10) {
setState(() {
_extended =
controller.position.userScrollDirection == ScrollDirection.forward;
});
}
prevPixelPosition = controller.position.pixels;
}
void _generatePDF() {
List<Business> allBusinesses = [];
for (List<Business> businessList
in _filterBySearch(overviewBusinesses, searchQuery).values) {
allBusinesses.addAll(businessList);
}
generatePDF(
context: context,
documentTypeIndex: 0,
selectedBusinesses: Set.from(allBusinesses));
}
@override
void initState() {
super.initState();
controller.addListener(_scrollListener);
}
@override
Widget build(BuildContext context) {
bool widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth;
return Scaffold(
floatingActionButton: _getFAB(widescreen),
body: CustomScrollView(
controller: controller,
slivers: [
MainSliverAppBar(
widescreen: widescreen,
setSearch: _setSearch,
searchHintText: 'Search Businesses',
themeCallback: widget.themeCallback,
filterIconButton: _filterIconButton(
businessTypeFilters,
),
updateLoggedIn: widget.updateLoggedIn,
generatePDF: _generatePDF,
),
FutureBuilder(
future: widget.refreshBusinessDataOverviewFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
if (snapshot.data.runtimeType == String) {
_isPreviousData = false;
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
Center(
child: Text(snapshot.data,
textAlign: TextAlign.center)),
Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Text('Retry'),
onPressed: () async {
if (!_isRetrying) {
setState(() {
_isRetrying = true;
});
await widget.updateBusinessesCallback(
businessTypeFilters);
}
},
),
),
]),
));
}
overviewBusinesses = snapshot.data;
_isPreviousData = true;
return BusinessDisplayPanel(
groupedBusinesses:
_filterBySearch(overviewBusinesses, searchQuery),
widescreen: widescreen,
);
} else if (snapshot.hasError) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Text(
'Error when loading data! Error: ${snapshot.error}'),
));
}
} else if (snapshot.connectionState ==
ConnectionState.waiting) {
if (_isPreviousData) {
return BusinessDisplayPanel(
groupedBusinesses:
_filterBySearch(overviewBusinesses, searchQuery),
widescreen: widescreen,
);
} else {
return SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: const SizedBox(
width: 75,
height: 75,
child: RiveAnimation.asset(
'assets/mdev_triangle_loading.riv'),
),
));
}
}
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'\nError: ${snapshot.error}',
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
),
);
}),
],
),
);
}
Widget _filterIconButton(Set<BusinessType> filters) {
Set<BusinessType> selectedChips = Set.from(filters);
return IconButton(
icon: Icon(
Icons.filter_list,
color: filters.isNotEmpty
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
onPressed: () {
selectedChips = Set.from(businessTypeFilters);
showDialog(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
void setDialogState(Set<BusinessType> newFilters) {
setState(() {
filters = newFilters;
});
}
List<Widget> chips = [];
for (var type in BusinessType.values) {
chips.add(FilterChip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
showCheckmark: false,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color:
Theme.of(context).colorScheme.secondary)),
selectedColor: Theme.of(context).colorScheme.secondary,
label: Text(
getNameFromBusinessType(type),
style: TextStyle(
color: selectedChips.contains(type)
? Theme.of(context).colorScheme.onSecondary
: Theme.of(context).colorScheme.onSurface),
),
selected: selectedChips.contains(type),
onSelected: (bool selected) {
if (selected) {
selectedChips.add(type);
} else {
selectedChips.remove(type);
}
setDialogState(filters);
}));
}
return AlertDialog(
title: const Text('Filter Options'),
content: SizedBox(
width: 400,
child: Wrap(
spacing: 8,
runSpacing: 8,
children: chips,
),
),
actions: [
TextButton(
child: const Text('Reset'),
onPressed: () {
_setFilters(<BusinessType>{});
// selectedChips = <BusinessType>{};
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Cancel'),
onPressed: () {
// selectedChips = Set.from(filters);
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Apply'),
onPressed: () {
_setFilters(selectedChips);
Navigator.of(context).pop();
},
)
],
);
});
});
});
}
Widget? _getFAB(bool widescreen) {
if (!widescreen && loggedIn) {
return FloatingActionButton.extended(
extendedIconLabelSpacing: _extended ? 8.0 : 0,
extendedPadding: const EdgeInsets.symmetric(horizontal: 16),
icon: const Icon(Icons.add),
label: AnimatedSize(
curve: Easing.standard,
duration: const Duration(milliseconds: 300),
child: _extended ? const Text('Add Business') : Container(),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CreateEditBusiness()));
},
);
}
return null;
}
}
class BusinessDisplayPanel extends StatefulWidget {
final Map<BusinessType, List<Business>> groupedBusinesses;
final bool widescreen;
const BusinessDisplayPanel({
super.key,
required this.groupedBusinesses,
required this.widescreen,
});
@override
State<BusinessDisplayPanel> createState() => _BusinessDisplayPanelState();
}
class _BusinessDisplayPanelState extends State<BusinessDisplayPanel> {
@override
Widget build(BuildContext context) {
if (widget.groupedBusinesses.keys.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'No results found!\nPlease change your search filters.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
),
),
);
}
List<_BusinessHeader> headers = [];
for (BusinessType businessType in widget.groupedBusinesses.keys) {
headers.add(_BusinessHeader(
businessType: businessType,
widescreen: widget.widescreen,
businesses: widget.groupedBusinesses[businessType]!));
}
headers
.sort((a, b) => a.businessType.index.compareTo(b.businessType.index));
return MultiSliver(children: headers);
}
}
class _BusinessHeader extends StatefulWidget {
final BusinessType businessType;
final List<Business> businesses;
final bool widescreen;
const _BusinessHeader({
required this.businessType,
required this.businesses,
required this.widescreen,
});
@override
State<_BusinessHeader> createState() => _BusinessHeaderState();
}
class _BusinessHeaderState extends State<_BusinessHeader> {
@override
Widget build(BuildContext context) {
return SliverStickyHeader(
header: Container(
height: 55.0,
color: Theme.of(context).colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
alignment: Alignment.centerLeft,
child: _getHeaderRow(),
),
sliver: _getChildSliver(widget.businesses, widget.widescreen),
);
}
Widget _getHeaderRow() {
return Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12.0),
child: Icon(
getIconFromBusinessType(widget.businessType),
color: Theme.of(context).colorScheme.onPrimary,
)),
Text(getNameFromBusinessType(widget.businessType),
style: TextStyle(color: Theme.of(context).colorScheme.onPrimary)),
],
);
}
Widget _getChildSliver(List<Business> businesses, bool widescreen) {
if (widescreen) {
return SliverPadding(
padding: const EdgeInsets.all(4),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
mainAxisExtent: 250.0,
maxCrossAxisExtent: 400.0,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
),
delegate: SliverChildBuilderDelegate(
childCount: businesses.length,
(BuildContext context, int index) {
return _businessTile(
businesses[index],
widget.businessType,
);
},
),
),
);
} else {
return SliverList(
delegate: SliverChildBuilderDelegate(
childCount: businesses.length,
(BuildContext context, int index) {
return _businessListItem(
businesses[index],
widget.businessType,
);
},
),
);
}
}
/// A desktop widget that displays basic info about a business
Widget _businessTile(Business business, BusinessType jobType) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => BusinessDetail(
id: business.id,
name: business.name!,
)));
},
child: Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 48,
width: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(getIconFromBusinessType(business.type!),
size: 48);
}),
)),
Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
business.name!,
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
business.description!,
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (business.website != null)
IconButton(
icon: const Icon(Icons.link),
onPressed: () {
launchUrl(Uri.parse(business.website!));
},
),
IconButton(
icon: const Icon(Icons.location_on),
onPressed: () {
launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName} ${business.locationAddress}')));
},
),
if (business.contactPhone != null)
IconButton(
icon: const Icon(Icons.phone),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.surface,
title:
Text('Contact ${business.contactName}'),
content: Text(
'Would you like to call or text ${business.contactName}?'),
actions: [
TextButton(
child: const Text('Text'),
onPressed: () {
launchUrl(Uri.parse(
'sms:${business.contactPhone}'));
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Call'),
onPressed: () async {
launchUrl(Uri.parse(
'tel:${business.contactPhone}'));
Navigator.of(context).pop();
}),
],
);
});
},
),
if (business.contactEmail != null)
IconButton(
icon: const Icon(Icons.email),
onPressed: () {
launchUrl(
Uri.parse('mailto:${business.contactEmail}'));
},
),
],
)),
],
),
),
),
);
}
/// A mobile widget that displays basic info about a business
Widget _businessListItem(Business business, BusinessType? jobType) {
return Card(
child: ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(3.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 24, width: 24, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(getIconFromBusinessType(business.type!));
})),
title: Text(business.name!),
subtitle: Text(business.description!,
maxLines: 2, overflow: TextOverflow.ellipsis),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => BusinessDetail(
id: business.id,
name: business.name!,
)));
},
),
);
}
}
+353 -286
View File
@@ -1,14 +1,14 @@
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/main.dart'; import 'package:fbla_ui/main.dart';
import 'package:fbla_ui/shared.dart'; import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class CreateEditBusiness extends StatefulWidget { class CreateEditBusiness extends StatefulWidget {
final Business? inputBusiness; final Business? inputBusiness;
final JobType? clickFromType;
const CreateEditBusiness({super.key, this.inputBusiness, this.clickFromType}); const CreateEditBusiness({super.key, this.inputBusiness});
@override @override
State<CreateEditBusiness> createState() => _CreateEditBusinessState(); State<CreateEditBusiness> createState() => _CreateEditBusinessState();
@@ -24,20 +24,23 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
late TextEditingController _notesController; late TextEditingController _notesController;
late TextEditingController _locationNameController; late TextEditingController _locationNameController;
late TextEditingController _locationAddressController; late TextEditingController _locationAddressController;
late bool widescreen;
Business business = Business( Business business = Business(
id: 0, id: 0,
name: 'Business', name: 'Business',
description: 'Add details about the business below.', description: 'Add details about the business below.',
website: '', type: null,
contactName: '', website: null,
contactEmail: '', contactName: null,
contactPhone: '', contactEmail: null,
notes: '', contactPhone: null,
notes: null,
locationName: '', locationName: '',
locationAddress: '', locationAddress: null,
); );
bool _isLoading = false; bool _isLoading = false;
String? dropDownErrorText;
@override @override
void initState() { void initState() {
@@ -47,11 +50,16 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
_nameController = TextEditingController(text: business.name); _nameController = TextEditingController(text: business.name);
_descriptionController = _descriptionController =
TextEditingController(text: business.description); TextEditingController(text: business.description);
business.type = widget.inputBusiness?.type;
} else { } else {
_nameController = TextEditingController(); _nameController = TextEditingController();
_descriptionController = TextEditingController(); _descriptionController = TextEditingController();
} }
_websiteController = TextEditingController(text: business.website); _websiteController = TextEditingController(
text: business.website
?.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''));
_contactNameController = TextEditingController(text: business.contactName); _contactNameController = TextEditingController(text: business.contactName);
_contactPhoneController = _contactPhoneController =
TextEditingController(text: business.contactPhone); TextEditingController(text: business.contactPhone);
@@ -65,10 +73,10 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
} }
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
final TextEditingController businessTypeController = TextEditingController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth;
return PopScope( return PopScope(
canPop: !_isLoading, canPop: !_isLoading,
onPopInvoked: _handlePop, onPopInvoked: _handlePop,
@@ -80,10 +88,14 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
? Text('Edit ${widget.inputBusiness?.name}', maxLines: 1) ? Text('Edit ${widget.inputBusiness?.name}', maxLines: 1)
: const Text('Add New Business'), : const Text('Add New Business'),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: !widescreen
child: _isLoading ? FloatingActionButton.extended(
? const Padding( heroTag: 'saveBusiness',
padding: EdgeInsets.all(16.0), label: const Text('Save'),
icon: _isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: Colors.white, color: Colors.white,
strokeWidth: 3.0, strokeWidth: 3.0,
@@ -91,88 +103,74 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
) )
: const Icon(Icons.save), : const Icon(Icons.save),
onPressed: () async { onPressed: () async {
if (formKey.currentState!.validate()) { if (!_isLoading) {
formKey.currentState?.save(); await _saveBusiness(context);
setState(() {
_isLoading = true;
});
String? result;
// if (business.contactName == '') {
// business.contactName = 'Contact ${business.name}';
// }
if (widget.inputBusiness != null) {
result = await editBusiness(business, jwt);
} else {
result = await createBusiness(business, jwt);
}
setState(() {
_isLoading = false;
});
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
width: 400,
behavior: SnackBarBehavior.floating,
content: Text(result)));
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const MainApp()));
}
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( const SnackBar(
content: const Text('Check field inputs!'), width: 400,
width: 200,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder( content: Text('Please wait for it to save.'),
borderRadius: BorderRadius.circular(10)),
), ),
); );
} }
}, },
), )
: null,
body: ListView( body: ListView(
children: [ children: [
Center( Center(
child: SizedBox( child: SizedBox(
width: 1000, width: 800,
child: Column( child: Column(
children: [ children: [
ListTile( Padding(
title: Text(business.name, padding: const EdgeInsets.only(top: 4.0),
textAlign: TextAlign.left, child: Card(
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ListTile(
titleAlignment:
ListTileTitleAlignment.titleHeight,
title: Text(business.name!,
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold)), fontSize: 24,
fontWeight: FontWeight.bold)),
subtitle: Text( subtitle: Text(
business.description, business.description!,
textAlign: TextAlign.left,
), ),
contentPadding:
const EdgeInsets.only(bottom: 8, left: 16),
leading: ClipRRect( leading: ClipRRect(
borderRadius: BorderRadius.circular(6.0), borderRadius: BorderRadius.circular(6.0),
child: Image.network( child: Image.network(
'$apiAddress/clearbit/${Uri.encodeComponent(business.website ?? '')}',
width: 48, width: 48,
height: 48, height: 48, errorBuilder:
'https://logo.clearbit.com/${business.website}', (BuildContext context,
errorBuilder: (BuildContext context, Object exception,
Object exception, StackTrace? stackTrace) { StackTrace? stackTrace) {
return getIconFromJobType( return Icon(
widget.clickFromType ?? JobType.other, getIconFromBusinessType(business.type ??
48, BusinessType.other),
Theme.of(context).colorScheme.onBackground); size: 48);
}), }),
), ),
), ),
),
),
),
Card( Card(
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 8.0, right: 8.0), top: 8.0,
bottom: 8.0,
left: 8.0,
right: 8.0),
child: TextFormField( child: TextFormField(
controller: _nameController, controller: _nameController,
autovalidateMode:
AutovalidateMode.onUserInteraction,
maxLength: 30, maxLength: 30,
onChanged: (inputName) { onChanged: (inputName) {
setState(() { setState(() {
@@ -186,7 +184,7 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
labelText: 'Business Name (required)', labelText: 'Business Name (required)',
), ),
validator: (value) { validator: (value) {
if (value != null && value.isEmpty) { if (value != null && value.trim().isEmpty) {
return 'Name is required'; return 'Name is required';
} }
return null; return null;
@@ -195,42 +193,9 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
), ),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0), bottom: 8.0, left: 8.0, right: 8.0),
child: TextFormField(
controller: _websiteController,
autovalidateMode:
AutovalidateMode.onUserInteraction,
keyboardType: TextInputType.url,
onChanged: (inputUrl) {
setState(() {
business.website = Uri.encodeFull(inputUrl
.toLowerCase()
.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''));
});
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Website (required)',
),
validator: (value) {
if (value != null && value.isEmpty) {
return 'Website is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0),
child: TextFormField( child: TextFormField(
controller: _descriptionController, controller: _descriptionController,
autovalidateMode:
AutovalidateMode.onUserInteraction,
maxLength: 500, maxLength: 500,
maxLines: null, maxLines: null,
onChanged: (inputDesc) { onChanged: (inputDesc) {
@@ -246,101 +211,207 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
'Business Description (required)', 'Business Description (required)',
), ),
validator: (value) { validator: (value) {
if (value != null && value.isEmpty) { if (value != null && value.trim().isEmpty) {
return 'Description is required'; return 'Description is required';
} }
return null; return null;
}, },
), ),
), ),
// Padding(
// padding: const EdgeInsets.only(
// left: 8.0, right: 8.0, bottom: 16.0),
// child: Row(
// children: [
// ElevatedButton(
// style: ButtonStyle(
// backgroundColor:
// MaterialStateProperty.all(
// Theme.of(context)
// .colorScheme
// .background)),
// child: const Row(
// children: [
// Icon(Icons.search),
// Text('Search For Location'),
// ],
// ),
// onPressed: () {},
// ),
// const Padding(
// padding: EdgeInsets.only(
// left: 32.0, right: 32.0),
// child: Text(
// 'OR',
// style: TextStyle(fontSize: 24),
// ),
// ),
// Expanded(
// child: Column(
// children: [
// TextFormField(
// controller: _locationNameController,
// onChanged: (inputName) {
// setState(() {
// business.locationName =
// inputName;
// });
// },
// onTapOutside:
// (PointerDownEvent event) {
// FocusScope.of(context).unfocus();
// },
// decoration: const InputDecoration(
// labelText:
// 'Location Name (optional)',
// ),
// ),
// TextFormField(
// controller:
// _locationAddressController,
// onChanged: (inputAddr) {
// setState(() {
// business.locationAddress =
// inputAddr;
// });
// },
// onTapOutside:
// (PointerDownEvent event) {
// FocusScope.of(context).unfocus();
// },
// decoration: const InputDecoration(
// labelText:
// 'Location Address (optional)',
// ),
// ),
// ],
// ),
// ),
// ],
// ),
// ),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0), left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _websiteController,
keyboardType: TextInputType.url,
onChanged: (inputUrl) {
setState(() {
business.website =
Uri.encodeFull(inputUrl);
});
if (inputUrl.trim().isEmpty) {
business.website = null;
} else {
if (!business.website!
.contains('http://') &&
!business.website!
.contains('https://')) {
setState(() {
business.website =
'https://${business.website!.trim()}';
});
}
}
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Website',
),
validator: (value) {
if (value != null &&
value.isNotEmpty &&
!RegExp(r'(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(?:/[^/\s]*)*')
.hasMatch(value)) {
return 'Enter a valid Website';
}
return null;
},
),
),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: DropdownMenu<BusinessType>(
initialSelection: business.type,
width: (MediaQuery.sizeOf(context).width -
24) <
776
? MediaQuery.sizeOf(context).width - 24
: 776,
menuHeight: 300,
label: const Text('Business Type'),
errorText: dropDownErrorText,
dropdownMenuEntries: [
for (BusinessType type
in BusinessType.values)
DropdownMenuEntry(
value: type,
label:
getNameFromBusinessType(type)),
],
onSelected: (inputType) {
setState(() {
business.type = inputType!;
dropDownErrorText = null;
});
},
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _contactNameController,
onSaved: (inputText) {
business.contactName = inputText!;
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText:
'Contact Information Name (required)',
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Contact name is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _contactPhoneController,
inputFormatters: [PhoneFormatter()],
keyboardType: TextInputType.phone,
onChanged: (inputText) {
if (inputText.trim().isEmpty) {
business.contactPhone = null;
} else {
business.contactPhone = inputText.trim();
}
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Contact Phone #',
),
validator: (value) {
if (business.contactEmail == null &&
(value == null || value.isEmpty)) {
return 'At least one contact method is required';
}
if (value != null &&
value.isNotEmpty &&
value.length != 14) {
return 'Enter a valid phone number';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _contactEmailController,
keyboardType: TextInputType.emailAddress,
onChanged: (inputText) {
if (inputText.trim().isEmpty) {
business.contactEmail = null;
} else {
business.contactEmail = inputText.trim();
}
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Contact Email',
),
validator: (value) {
value = value?.trim();
if (value != null && value.isEmpty) {
value = null;
}
if (value == null &&
business.contactPhone == null) {
return 'At least one contact method is required';
}
if (value != null) {
if (!RegExp(
r'^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')
.hasMatch(value)) {
return 'Enter a valid Email';
} else if (value.characters.length > 50) {
return 'Contact Email cannot be longer than 50 characters';
}
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField( child: TextFormField(
controller: _locationNameController, controller: _locationNameController,
onChanged: (inputName) { onChanged: (inputName) {
setState(() { setState(() {
business.locationName = inputName; business.locationName = inputName.trim();
}); });
}, },
onTapOutside: (PointerDownEvent event) { onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
}, },
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Location Name', labelText: 'Location Name (required)',
), ),
validator: (value) {
if (value != null && value.trim().isEmpty) {
return 'Location name is required';
}
return null;
},
), ),
), ),
Padding( Padding(
@@ -357,113 +428,11 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
}, },
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Location Address', labelText: 'Location Address (required)',
),
),
),
// Business Type Dropdown
// Padding(
// padding: const EdgeInsets.only(
// left: 8.0, right: 8.0, bottom: 8.0),
// child: Row(
// mainAxisAlignment:
// MainAxisAlignment.spaceBetween,
// children: [
// const Text('Type of Business',
// style: TextStyle(fontSize: 16)),
// DropdownMenu<BusinessType>(
// initialSelection: business.type,
// controller: businessTypeController,
// label: const Text('Business Type'),
// dropdownMenuEntries: const [
// DropdownMenuEntry(
// value: BusinessType.food,
// label: 'Food Related'),
// DropdownMenuEntry(
// value: BusinessType.shop,
// label: 'Shop'),
// DropdownMenuEntry(
// value: BusinessType.outdoors,
// label: 'Outdoors'),
// DropdownMenuEntry(
// value: BusinessType.manufacturing,
// label: 'Manufacturing'),
// DropdownMenuEntry(
// value: BusinessType.entertainment,
// label: 'Entertainment'),
// DropdownMenuEntry(
// value: BusinessType.other,
// label: 'Other'),
// ],
// onSelected: (inputType) {
// business.type = inputType!;
// },
// ),
// ],
// ),
// ),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: TextFormField(
controller: _contactNameController,
onSaved: (inputText) {
business.contactName = inputText!;
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Contact Information Name',
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: TextFormField(
controller: _contactPhoneController,
inputFormatters: [PhoneFormatter()],
keyboardType: TextInputType.phone,
onSaved: (inputText) {
business.contactPhone = inputText!;
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Contact Phone # (optional)',
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: TextFormField(
controller: _contactEmailController,
keyboardType: TextInputType.emailAddress,
onSaved: (inputText) {
business.contactEmail = inputText!;
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Contact Email',
), ),
validator: (value) { validator: (value) {
if (value != null) { if (value != null && value.trim().isEmpty) {
if (value.isEmpty) { return 'Location Address is required';
return null;
} else if (!RegExp(
r'^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')
.hasMatch(value)) {
return 'Enter a valid Email';
} else if (value.characters.length > 50) {
return 'Contact Email cannot be longer than 50 characters';
}
} }
return null; return null;
}, },
@@ -477,7 +446,12 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
maxLength: 300, maxLength: 300,
maxLines: null, maxLines: null,
onSaved: (inputText) { onSaved: (inputText) {
business.notes = inputText!; if (inputText == null ||
inputText.trim().isEmpty) {
business.notes = null;
} else {
business.notes = inputText.trim();
}
}, },
onTapOutside: (PointerDownEvent event) { onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
@@ -490,9 +464,55 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
], ],
), ),
), ),
SizedBox( if (!widescreen)
const SizedBox(
height: 75, height: 75,
) )
else
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(
top: 8.0, right: 8.0, bottom: 8.0),
child: _isLoading
? SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Theme.of(context)
.colorScheme
.onPrimary,
strokeWidth: 3.0,
),
)
: const Icon(Icons.save),
),
const Text('Save'),
],
),
onPressed: () async {
if (!_isLoading) {
await _saveBusiness(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
width: 400,
behavior: SnackBarBehavior.floating,
content:
Text('Please wait for it to save.'),
),
);
}
},
),
),
)
], ],
), ),
), ),
@@ -514,6 +534,53 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
); );
} }
} }
Future<void> _saveBusiness(BuildContext context) async {
if (business.type == null) {
setState(() {
dropDownErrorText = 'Business type is required';
});
formKey.currentState!.validate();
} else {
setState(() {
dropDownErrorText = null;
});
if (formKey.currentState!.validate()) {
formKey.currentState?.save();
setState(() {
_isLoading = true;
});
String? result;
if (widget.inputBusiness != null) {
result = await editBusiness(business);
} else {
result = await createBusiness(business);
}
setState(() {
_isLoading = false;
});
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
width: 400,
behavior: SnackBarBehavior.floating,
content: Text(result)));
} else {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => const MainApp()));
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Check field inputs!'),
width: 200,
behavior: SnackBarBehavior.floating,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
}
}
} }
class PhoneFormatter extends TextInputFormatter { class PhoneFormatter extends TextInputFormatter {
+634
View File
@@ -0,0 +1,634 @@
import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import '../main.dart';
class CreateEditJobListing extends StatefulWidget {
final JobListing? inputJobListing;
final Business? inputBusiness;
const CreateEditJobListing(
{super.key, this.inputJobListing, this.inputBusiness});
@override
State<CreateEditJobListing> createState() => _CreateEditJobListingState();
}
class _CreateEditJobListingState extends State<CreateEditJobListing> {
late Future getBusinessNameMapping;
late TextEditingController _nameController;
late TextEditingController _descriptionController;
late TextEditingController _wageController;
late TextEditingController _linkController;
List<Map<String, dynamic>> nameMapping = [];
String? typeDropdownErrorText;
String? businessDropdownErrorText;
late bool widescreen;
JobListing listing = JobListing(
id: null,
businessId: null,
name: 'Job Listing',
description: 'Add details about the job below.',
type: null,
wage: null,
link: null,
offerType: null);
bool _isLoading = false;
late String businessName;
bool _isRetrying = false;
@override
void initState() {
super.initState();
if (widget.inputJobListing != null) {
listing = JobListing.copy(widget.inputJobListing!);
_nameController = TextEditingController(text: listing.name);
_descriptionController = TextEditingController(text: listing.description);
} else {
_nameController = TextEditingController();
_descriptionController = TextEditingController();
}
_wageController = TextEditingController(text: listing.wage);
_linkController = TextEditingController(
text: listing.link
?.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''));
getBusinessNameMapping = fetchBusinessNames();
businessName = widget.inputBusiness?.name ?? 'Offering business';
if (widget.inputBusiness != null) {
listing.businessId = widget.inputBusiness!.id;
}
}
final formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth;
return PopScope(
canPop: !_isLoading,
onPopInvoked: _handlePop,
child: Form(
key: formKey,
child: Scaffold(
appBar: AppBar(
title: (widget.inputJobListing != null)
? Text('Edit ${widget.inputJobListing?.name}', maxLines: 1)
: const Text('Add New Job Listing'),
),
floatingActionButton: !widescreen
? FloatingActionButton.extended(
heroTag: 'saveListing',
label: const Text('Save'),
icon: _isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3.0,
),
)
: const Icon(Icons.save),
onPressed: () async {
if (!_isLoading) {
await _saveListing(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
width: 400,
behavior: SnackBarBehavior.floating,
content: Text('Please wait for it to save.'),
),
);
}
})
: null,
body: FutureBuilder(
future: getBusinessNameMapping,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
if (snapshot.data.runtimeType == String) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
Center(
child: Text(snapshot.data,
textAlign: TextAlign.center)),
Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Text('Retry'),
onPressed: () async {
if (!_isRetrying) {
setState(() {
_isRetrying = true;
});
var refreshedData = fetchBusinessNames();
await refreshedData;
setState(() {
getBusinessNameMapping = refreshedData;
_isRetrying = false;
});
}
},
),
),
]),
);
}
nameMapping = snapshot.data;
nameMapping.sort((a, b) =>
a['name'].toString().compareTo(b['name'].toString()));
return ListView(
children: [
Center(
child: SizedBox(
width: 800,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Card(
child: Padding(
padding:
const EdgeInsets.only(right: 8.0),
child: ListTile(
titleAlignment: ListTileTitleAlignment
.titleHeight,
title: Text(listing.name,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
businessName,
style: const TextStyle(
fontSize: 18),
),
Text(
listing.description,
),
],
),
contentPadding:
const EdgeInsets.only(left: 16),
leading: ClipRRect(
borderRadius:
BorderRadius.circular(6.0),
child: Image.network(
'$apiAddress/logos/${listing.businessId}',
width: 48,
height: 48, errorBuilder:
(BuildContext context,
Object exception,
StackTrace?
stackTrace) {
return Icon(
getIconFromJobType(
listing.type ??
JobType.other),
size: 48);
}),
),
),
),
),
),
// Business Type Dropdown
Card(
child: Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 8.0,
top: 8.0),
child: DropdownMenu<int>(
menuHeight: 300,
width: (MediaQuery.sizeOf(context)
.width -
24) <
776
? MediaQuery.sizeOf(context)
.width -
24
: 776,
errorText:
businessDropdownErrorText,
initialSelection:
widget.inputBusiness?.id,
label: const Text(
'Offering Business'),
dropdownMenuEntries: [
for (Map<String, dynamic> map
in nameMapping)
DropdownMenuEntry(
value: map['id']!,
label: map['name'])
],
onSelected: (inputType) {
setState(() {
listing.businessId =
inputType!;
businessName = nameMapping
.where((element) =>
element['id'] ==
listing.businessId)
.first['name'];
businessDropdownErrorText =
null;
});
},
),
),
),
Align(
alignment: Alignment.centerLeft,
child: Wrap(
children: [
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 16.0,
top: 8.0),
child: DropdownMenu<JobType>(
initialSelection:
listing.type,
label: const Text('Job Type'),
errorText:
typeDropdownErrorText,
width: calculateDropdownWidth(
context),
menuHeight: 300,
dropdownMenuEntries: [
for (JobType type
in JobType.values)
DropdownMenuEntry(
value: type,
label:
getNameFromJobType(
type))
],
onSelected: (inputType) {
setState(() {
listing.type = inputType!;
typeDropdownErrorText =
null;
});
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 8.0,
top: 8.0),
child: DropdownMenu<OfferType>(
initialSelection:
listing.offerType,
label:
const Text('Offer Type'),
errorText:
typeDropdownErrorText,
width: calculateDropdownWidth(
context),
dropdownMenuEntries: [
for (OfferType type
in OfferType.values)
DropdownMenuEntry(
value: type,
label:
getNameFromOfferType(
type))
],
onSelected: (inputType) {
setState(() {
listing.offerType =
inputType!;
typeDropdownErrorText =
null;
});
},
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 8.0),
child: TextFormField(
controller: _nameController,
maxLength: 40,
onChanged: (inputName) {
setState(() {
listing.name = inputName;
});
},
onTapOutside:
(PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText:
'Job Listing Name (required)',
),
validator: (value) {
if (value != null &&
value.isEmpty) {
return 'Name is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 8.0),
child: TextFormField(
controller: _descriptionController,
maxLength: 500,
maxLines: null,
onChanged: (inputDesc) {
setState(() {
listing.description = inputDesc;
});
},
onTapOutside:
(PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText:
'Job Listing Description (required)',
),
validator: (value) {
if (value != null &&
value.isEmpty) {
return 'Description is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 16.0),
child: TextFormField(
controller: _wageController,
onChanged: (input) {
setState(() {
listing.wage = input;
});
},
onTapOutside:
(PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Wage Information',
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 16.0),
child: TextFormField(
controller: _linkController,
keyboardType: TextInputType.url,
onChanged: (inputUrl) {
if (inputUrl != '') {
listing.link =
Uri.encodeFull(inputUrl);
if (!listing.link!
.contains('http://') &&
!listing.link!
.contains('https://')) {
listing.link =
'https://${listing.link}';
}
} else {
listing.link = null;
}
},
validator: (value) {
if (value != null &&
value.isNotEmpty &&
!RegExp(r'(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(?:/[^/\s]*)*')
.hasMatch(value)) {
return 'Enter a valid Website';
}
return null;
},
onTapOutside:
(PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText:
'Additional Information Link',
),
),
),
],
),
),
if (!widescreen)
const SizedBox(
height: 75,
)
else
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(
top: 8.0,
right: 8.0,
bottom: 8.0),
child: _isLoading
? SizedBox(
width: 24,
height: 24,
child:
CircularProgressIndicator(
color:
Theme.of(context)
.colorScheme
.onPrimary,
strokeWidth: 3.0,
),
)
: const Icon(Icons.save),
),
const Text('Save'),
],
),
onPressed: () async {
if (!_isLoading) {
await _saveListing(context);
} else {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
width: 400,
behavior:
SnackBarBehavior.floating,
content: Text(
'Please wait for it to save.'),
),
);
}
},
),
),
)
],
),
),
),
],
);
} else if (snapshot.hasError) {
return Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Text(
'Error when loading data! Error: ${snapshot.error}'),
);
}
} else if (snapshot.connectionState ==
ConnectionState.waiting) {
return Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: const SizedBox(
width: 75,
height: 75,
child: RiveAnimation.asset(
'assets/mdev_triangle_loading.riv'),
),
);
}
return const Padding(
padding: EdgeInsets.only(left: 16.0, right: 16.0),
child: Text('Error when loading data!'),
);
}),
)),
);
}
double calculateDropdownWidth(BuildContext context) {
double screenWidth = MediaQuery.sizeOf(context).width;
if ((screenWidth - 40) / 2 < 200) {
return screenWidth - 24;
} else if ((screenWidth - 40) / 2 < 380) {
return (screenWidth - 40) / 2;
} else {
return 380;
}
}
void _handlePop(bool didPop) {
if (!didPop) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
width: 400,
behavior: SnackBarBehavior.floating,
content: Text('Please wait for it to save.'),
),
);
}
}
Future<void> _saveListing(BuildContext context) async {
if (listing.type == null || listing.businessId == null) {
if (listing.type == null) {
setState(() {
typeDropdownErrorText = 'Job type is required';
});
formKey.currentState!.validate();
}
if (listing.businessId == null) {
setState(() {
businessDropdownErrorText = 'Business is required';
});
formKey.currentState!.validate();
}
} else {
setState(() {
typeDropdownErrorText = null;
businessDropdownErrorText = null;
});
if (formKey.currentState!.validate()) {
formKey.currentState?.save();
setState(() {
_isLoading = true;
});
String? result;
if (widget.inputJobListing != null) {
result = await editListing(listing);
} else {
result = await createListing(listing);
}
setState(() {
_isLoading = false;
});
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
width: 400,
behavior: SnackBarBehavior.floating,
content: Text(result)));
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const MainApp(
initialPage: 1,
)));
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Check field inputs!'),
width: 200,
behavior: SnackBarBehavior.floating,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
}
}
}
-585
View File
@@ -1,585 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/shared.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:open_filex/open_filex.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
bool isDataTypesFiltered = false;
bool isBusinessesFiltered = true;
bool _isLoading = false;
class ExportData extends StatefulWidget {
final Map<JobType, List<Business>> groupedBusinesses;
const ExportData({super.key, required this.groupedBusinesses});
@override
State<ExportData> createState() => _ExportDataState();
}
class _ExportDataState extends State<ExportData> {
late Future refreshBusinessDataFuture;
@override
void initState() {
super.initState();
refreshBusinessDataFuture = fetchBusinessDataOverview();
_isLoading = false;
selectedBusinesses = <Business>{};
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: _FAB(groupedBusinesses: widget.groupedBusinesses),
body: CustomScrollView(
slivers: [
SliverAppBar(
forceMaterialTransparency: false,
title: const Text('Export Data'),
toolbarHeight: 70,
pinned: true,
centerTitle: true,
expandedHeight: 120,
backgroundColor: Theme.of(context).colorScheme.background,
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
// DO NOT MOVE TO SEPARATE WIDGET, setState is needed in main tree
backgroundColor:
Theme.of(context).colorScheme.background,
title: const Text('Data Types'),
content: const SizedBox(
width: 400,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Data Columns you would like to show on the datasheet'),
FilterDataTypeChips(),
],
),
),
actions: [
TextButton(
child: const Text('Reset'),
onPressed: () {
setState(() {
dataTypeFilters = <DataType>{};
selectedDataTypes = <DataType>{};
isDataTypesFiltered = false;
});
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Cancel'),
onPressed: () {
selectedDataTypes = Set.from(dataTypeFilters);
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Apply'),
onPressed: () {
setState(() {
selectedDataTypes =
sortDataTypes(selectedDataTypes);
dataTypeFilters =
Set.from(selectedDataTypes);
if (dataTypeFilters.isNotEmpty) {
isDataTypesFiltered = true;
} else {
isDataTypesFiltered = false;
}
});
Navigator.of(context).pop();
}),
],
);
});
},
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(0),
child: SizedBox(
height: 70,
width: 1000,
child: Padding(
padding: const EdgeInsets.all(10),
child: TextField(
onChanged: (query) {
setState(() {
searchFilter = query;
});
},
decoration: InputDecoration(
labelText: 'Search',
hintText: 'Search',
prefixIcon: const Padding(
padding: EdgeInsets.only(left: 8.0),
child: Icon(Icons.search),
),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(25.0)),
),
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
icon: Icon(Icons.filter_list,
color: isFiltered
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onBackground),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
// DO NOT MOVE TO SEPARATE WIDGET, setState is needed in main tree
backgroundColor: Theme.of(context)
.colorScheme
.background,
title: const Text('Filter Options'),
content: const FilterChips(),
actions: [
TextButton(
child: const Text('Reset'),
onPressed: () {
setState(() {
filters = <BusinessType>{};
selectedChips = <BusinessType>{};
isFiltered = false;
});
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Cancel'),
onPressed: () {
selectedChips = Set.from(filters);
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Apply'),
onPressed: () {
setState(() {
filters = selectedChips;
if (filters.isNotEmpty) {
isFiltered = true;
} else {
isFiltered = false;
}
});
Navigator.of(context).pop();
}),
],
);
});
},
),
),
),
),
),
),
),
),
BusinessDisplayPanel(
groupedBusinesses: widget.groupedBusinesses,
widescreen: MediaQuery.sizeOf(context).width >= 1000,
selectable: true),
const SliverToBoxAdapter(
child: SizedBox(
height: 100,
),
),
],
),
);
}
}
class _FAB extends StatefulWidget {
final Map<JobType, List<Business>> groupedBusinesses;
const _FAB({required this.groupedBusinesses});
@override
State<_FAB> createState() => _FABState();
}
class _FABState extends State<_FAB> {
List<Business> allBusinesses = [];
@override
void initState() {
super.initState();
for (JobType type in widget.groupedBusinesses.keys) {
allBusinesses.addAll(widget.groupedBusinesses[type]!);
}
}
@override
Widget build(BuildContext context) {
return FloatingActionButton(
child: _isLoading
? const Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3.0,
),
)
: const Icon(Icons.save_alt),
onPressed: () async {
setState(() {
_isLoading = true;
});
try {
DateTime dateTime = DateTime.now();
String minute = '00';
if (dateTime.minute.toString().length < 2) {
minute = '0${dateTime.minute}';
} else {
minute = dateTime.minute.toString();
}
String time = dateTime.hour <= 13
? '${dateTime.hour}:${minute}AM'
: '${dateTime.hour - 12}:${minute}PM';
String fileName =
'Business Data - ${dateTime.month}-${dateTime.day}-${dateTime.year} $time.pdf';
final pdf = pw.Document();
var svgBytes = await marinoDevLogo();
selectedDataTypes = sortDataTypes(selectedDataTypes);
List<pw.Padding> headers = [];
if (selectedDataTypes.isEmpty) {
dataTypeFilters.addAll(DataType.values);
} else {
for (var filter in selectedDataTypes) {
dataTypeFilters.add(filter);
}
}
for (var filter in dataTypeFilters) {
headers.add(pw.Padding(
child: pw.Text(dataTypeFriendly[filter]!,
style: const pw.TextStyle(fontSize: 10)),
padding: const pw.EdgeInsets.all(4.0)));
}
List<pw.TableRow> rows = [];
if (selectedBusinesses.isEmpty) {
selectedBusinesses.addAll(allBusinesses);
isBusinessesFiltered = false;
} else {
isBusinessesFiltered = true;
}
double remainingSpace = 744;
if (dataTypeFilters.contains(DataType.logo)) {
remainingSpace -= 32;
}
// if (dataTypeFilters.contains(DataType.type)) {
// remainingSpace -= 56;
// }
if (dataTypeFilters.contains(DataType.contactName)) {
remainingSpace -= 72;
}
if (dataTypeFilters.contains(DataType.contactPhone)) {
remainingSpace -= 76;
}
double nameWidth = 0;
double websiteWidth = 0;
double contactEmailWidth = 0;
double notesWidth = 0;
double descriptionWidth = 0;
if (dataTypeFilters.contains(DataType.name)) {
nameWidth = (remainingSpace / 6);
}
if (dataTypeFilters.contains(DataType.website)) {
websiteWidth = (remainingSpace / 5);
}
if (dataTypeFilters.contains(DataType.contactEmail)) {
contactEmailWidth = (remainingSpace / 5);
}
if (dataTypeFilters.contains(DataType.notes)) {
notesWidth = (remainingSpace / 7);
}
remainingSpace -=
(nameWidth + websiteWidth + contactEmailWidth + notesWidth);
if (dataTypeFilters.contains(DataType.description)) {
descriptionWidth = remainingSpace;
}
Map<int, pw.TableColumnWidth> columnWidths = {};
int columnNum = -1;
for (var dataType in dataTypeFilters) {
pw.TableColumnWidth width = const pw.FixedColumnWidth(0);
if (dataType == DataType.logo) {
width = const pw.FixedColumnWidth(32);
columnNum++;
} else if (dataType == DataType.name) {
width = pw.FixedColumnWidth(nameWidth);
columnNum++;
} else if (dataType == DataType.description) {
width = pw.FixedColumnWidth(descriptionWidth);
columnNum++;
// } else if (dataType == DataType.type) {
// width = const pw.FixedColumnWidth(56);
// columnNum++;
} else if (dataType == DataType.website) {
width = pw.FixedColumnWidth(websiteWidth);
columnNum++;
} else if (dataType == DataType.contactName) {
width = const pw.FixedColumnWidth(72);
columnNum++;
} else if (dataType == DataType.contactEmail) {
width = pw.FixedColumnWidth(contactEmailWidth);
columnNum++;
} else if (dataType == DataType.contactPhone) {
width = const pw.FixedColumnWidth(76);
columnNum++;
} else if (dataType == DataType.notes) {
width = pw.FixedColumnWidth(notesWidth);
columnNum++;
}
columnWidths.addAll({columnNum: width});
}
for (var business in selectedBusinesses) {
List<pw.Padding> data = [];
bool hasLogo = false;
Uint8List businessLogo = Uint8List(0);
if (dataTypeFilters.contains(DataType.logo)) {
try {
var apiLogo = await getLogo(business.id);
if (apiLogo.runtimeType != String) {
businessLogo = apiLogo;
hasLogo = true;
}
} catch (e) {
if (kDebugMode) {
print('Logo not available! $e');
}
}
}
if (dataTypeFilters.contains(DataType.name)) {
data.add(pw.Padding(
child: pw.Text(
business.name,
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
}
if (dataTypeFilters.contains(DataType.description)) {
pw.TextStyle style = const pw.TextStyle(fontSize: 9);
if (business.description.length >= 200) {
style = const pw.TextStyle(fontSize: 8);
}
if (business.description.length >= 400) {
style = const pw.TextStyle(fontSize: 7);
}
data.add(pw.Padding(
child: pw.Text(
business.description,
style: style,
),
padding: const pw.EdgeInsets.all(4.0)));
}
// if (dataTypeFilters.contains(DataType.type)) {
// data.add(pw.Padding(
// child: pw.Text(
// business.type.name,
// // style: const pw.TextStyle(fontSize: 10)
// ),
// padding: const pw.EdgeInsets.all(4.0)));
// }
if (dataTypeFilters.contains(DataType.website)) {
data.add(pw.Padding(
child: pw.Text(
business.website ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
}
if (dataTypeFilters.contains(DataType.contactName)) {
data.add(pw.Padding(
child: pw.Text(
business.contactName ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
}
if (dataTypeFilters.contains(DataType.contactEmail)) {
data.add(pw.Padding(
child: pw.Text(
business.contactEmail ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
}
if (dataTypeFilters.contains(DataType.contactPhone)) {
data.add(pw.Padding(
child: pw.Text(
business.contactPhone ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
}
if (dataTypeFilters.contains(DataType.notes)) {
pw.TextStyle style = const pw.TextStyle(fontSize: 9);
if (business.description.length >= 200) {
style = const pw.TextStyle(fontSize: 8);
}
data.add(pw.Padding(
child: pw.Text(business.notes ?? '', style: style),
padding: const pw.EdgeInsets.all(4.0)));
}
if (dataTypeFilters.contains(DataType.logo)) {
if (hasLogo) {
rows.add(pw.TableRow(
children: [
pw.Padding(
child: pw.ClipRRect(
child: pw.Image(pw.MemoryImage(businessLogo),
height: 24, width: 24),
horizontalRadius: 4,
verticalRadius: 4),
padding: const pw.EdgeInsets.all(4.0)),
...data
],
));
} else {
rows.add(pw.TableRow(
children: [
// pw.Padding(
// child: getPwIconFromType(
// business.type, 24, PdfColors.black),
// padding: const pw.EdgeInsets.all(4.0)),
...data
],
));
}
} else {
rows.add(pw.TableRow(
children: data,
));
}
}
var themeIcon = pw.ThemeData.withFont(
base: await PdfGoogleFonts.notoSansDisplayMedium(),
icons: await PdfGoogleFonts.materialIcons());
var finaltheme = themeIcon.copyWith(
defaultTextStyle: const pw.TextStyle(fontSize: 9),
);
pdf.addPage(pw.MultiPage(
theme: finaltheme,
// theme: pw.ThemeData(
// tableCell: const pw.TextStyle(fontSize: 4),
// defaultTextStyle: const pw.TextStyle(fontSize: 4),
// header0: const pw.TextStyle(fontSize: 4),
// paragraphStyle: const pw.TextStyle(fontSize: 4),
// ),
// theme: pw.ThemeData.withFont(
// icons: await PdfGoogleFonts.materialIcons()),
pageFormat: PdfPageFormat.letter,
orientation: pw.PageOrientation.landscape,
margin: const pw.EdgeInsets.all(24),
build: (pw.Context context) {
return [
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.SvgImage(svg: utf8.decode(svgBytes), height: 40),
pw.Padding(
padding: const pw.EdgeInsets.all(8.0),
child: pw.Text('Business Datasheet',
style: pw.TextStyle(
fontSize: 32,
fontWeight: pw.FontWeight.bold)),
),
pw.Text(
'Generated on ${dateTime.month}/${dateTime.day}/${dateTime.year} at $time',
style: const pw.TextStyle(fontSize: 12),
textAlign: pw.TextAlign.right),
//
]),
pw.Table(
columnWidths: columnWidths,
// defaultColumnWidth: pw.IntrinsicColumnWidth(),
border: const pw.TableBorder(
bottom: pw.BorderSide(),
left: pw.BorderSide(),
right: pw.BorderSide(),
top: pw.BorderSide(),
horizontalInside: pw.BorderSide(),
verticalInside: pw.BorderSide()),
children: [
pw.TableRow(
decoration:
const pw.BoxDecoration(color: PdfColors.blue400),
children: headers,
repeat: true,
),
...rows,
]),
];
}));
Uint8List pdfBytes = await pdf.save();
if (kIsWeb) {
await Printing.sharePdf(
bytes: await pdf.save(),
filename: fileName,
);
} else {
var dir = await getTemporaryDirectory();
var tempDir = dir.path;
File pdfFile = File('$tempDir/$fileName');
pdfFile.writeAsBytesSync(pdfBytes);
OpenFilex.open(pdfFile.path);
}
if (!isBusinessesFiltered) {
selectedBusinesses = <Business>{};
}
setState(() {
_isLoading = false;
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Error generating PDF! $e'),
width: 300,
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 2),
));
}
},
);
}
}
+212
View File
@@ -0,0 +1,212 @@
import 'package:fbla_ui/main.dart';
import 'package:fbla_ui/pages/business_detail.dart';
import 'package:fbla_ui/pages/create_edit_listing.dart';
import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:fbla_ui/shared/widgets.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
/// A page to view all specific details about a single job listing (with its businesses contact info)
class JobListingDetail extends StatefulWidget {
final JobListing listing;
final Business fromBusiness;
const JobListingDetail(
{super.key, required this.listing, required this.fromBusiness});
@override
State<JobListingDetail> createState() => _CreateBusinessDetailState();
}
class _CreateBusinessDetailState extends State<JobListingDetail> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.listing.name),
actions: _getActions(widget.listing, widget.fromBusiness),
),
body: _detailBody(widget.listing),
);
}
/// body of the JobListingDetail
Widget _detailBody(JobListing listing) {
return ListView(
children: [
Center(
child: SizedBox(
width: 800,
child: Column(
children: [
// Top summary card
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: _summaryCard(listing)),
// Wage card
if (listing.wage != null && listing.wage != '')
Card(
child: ListTile(
leading: const Icon(Icons.attach_money),
subtitle: Text(listing.wage!),
title: const Text('Wage Information'),
),
),
// Contact information for the business contact
ContactInformationCard(business: widget.fromBusiness)
],
),
),
),
],
);
}
/// Top card including title, logo, description, and business name
Widget _summaryCard(JobListing listing) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ListTile(
minVerticalPadding: 0,
titleAlignment: ListTileTitleAlignment.titleHeight,
title: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'${listing.name} (${getNameFromOfferType(listing.offerType!)})',
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold)),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) {
return BusinessDetail(
id: widget.fromBusiness.id,
name: widget.fromBusiness.name!);
}));
},
child: Text(
widget.fromBusiness.name!,
style: const TextStyle(fontSize: 18),
),
),
),
Text(listing.description),
],
),
contentPadding: const EdgeInsets.only(bottom: 8, left: 16),
leading: Badge(
label: Text(
getLetterFromOfferType(listing.offerType!),
style: const TextStyle(fontSize: 16),
),
largeSize: 24,
offset: const Offset(12, -3),
textColor: Colors.white,
backgroundColor: getColorFromOfferType(listing.offerType!),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network(
'$apiAddress/logos/${widget.fromBusiness.id}',
width: 48,
height: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(
getIconFromJobType(listing.type ?? JobType.other),
size: 48);
}),
),
),
),
),
if (listing.link != null && listing.link != '')
ListTile(
leading: const Icon(Icons.link),
title: const Text('More Information'),
subtitle: Text(
listing.link!
.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''),
style: const TextStyle(color: Colors.blue)),
onTap: () {
launchUrl(Uri.parse(listing.link!));
},
),
],
),
);
}
/// Edit / delete actions if the user is logged in
List<Widget>? _getActions(JobListing listing, Business fromBusiness) {
if (loggedIn) {
return [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => CreateEditJobListing(
inputJobListing: listing,
inputBusiness: fromBusiness,
)));
},
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Theme.of(context).colorScheme.surface,
title: const Text('Are You Sure?'),
content:
Text('This will permanently delete ${listing.name}.'),
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Yes'),
onPressed: () async {
String? deleteResult =
await deleteListing(listing.id!);
if (deleteResult != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(deleteResult)));
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const MainApp(
initialPage: 1,
)));
}
}),
],
);
});
},
),
];
}
return null;
}
}
+696
View File
@@ -0,0 +1,696 @@
import 'package:fbla_ui/pages/create_edit_listing.dart';
import 'package:fbla_ui/pages/listing_detail.dart';
import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/export.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:fbla_ui/shared/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:rive/rive.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:url_launcher/url_launcher.dart';
class JobsOverview extends StatefulWidget {
final String searchQuery;
final Future refreshJobDataOverviewFuture;
final Future<void> Function(Set<JobType>?, Set<OfferType>?)
updateBusinessesCallback;
final void Function() themeCallback;
final void Function(bool) updateLoggedIn;
const JobsOverview({
super.key,
required this.searchQuery,
required this.refreshJobDataOverviewFuture,
required this.updateBusinessesCallback,
required this.themeCallback,
required this.updateLoggedIn,
});
@override
State<JobsOverview> createState() => _JobsOverviewState();
}
class _JobsOverviewState extends State<JobsOverview> {
bool _isPreviousData = false;
late Map<JobType, List<Business>> overviewBusinesses;
Set<JobType> jobTypeFilters = <JobType>{};
Set<OfferType> offerTypeFilters = <OfferType>{};
String searchQuery = '';
ScrollController controller = ScrollController();
bool _extended = true;
double prevPixelPosition = 0;
bool _isRetrying = false;
Map<JobType, List<Business>> _filterBySearch(
Map<JobType, List<Business>> businesses, String query) {
Map<JobType, List<Business>> filteredBusinesses = {};
for (JobType jobType in businesses.keys) {
filteredBusinesses[jobType] = List.from(businesses[jobType]!.where(
(element) => element.listings![0].name
.replaceAll(RegExp(r'[^a-zA-Z]'), '')
.toLowerCase()
.contains(query
.replaceAll(RegExp(r'[^a-zA-Z]'), '')
.toLowerCase()
.trim())));
}
filteredBusinesses.removeWhere((key, value) => value.isEmpty);
return filteredBusinesses;
}
void _setSearch(String search) async {
setState(() {
searchQuery = search;
});
}
void _setFilters(Set<JobType>? newJobTypeFilters,
Set<OfferType>? newOfferTypeFilters) async {
if (newJobTypeFilters != null) {
jobTypeFilters = Set.from(newJobTypeFilters);
}
if (newOfferTypeFilters != null) {
offerTypeFilters = Set.from(newOfferTypeFilters);
}
widget.updateBusinessesCallback(jobTypeFilters, offerTypeFilters);
}
void _scrollListener() {
if ((prevPixelPosition - controller.position.pixels).abs() > 10) {
setState(() {
_extended =
controller.position.userScrollDirection == ScrollDirection.forward;
});
}
prevPixelPosition = controller.position.pixels;
}
void _generatePDF() {
List<Business> allJobs = [];
for (List<Business> businesses
in _filterBySearch(overviewBusinesses, searchQuery).values) {
allJobs.addAll(businesses);
}
generatePDF(
context: context,
documentTypeIndex: 1,
selectedJobs: Set.from(allJobs));
}
@override
void initState() {
super.initState();
controller.addListener(_scrollListener);
}
@override
Widget build(BuildContext context) {
bool widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth;
return Scaffold(
floatingActionButton: _getFAB(widescreen),
body: CustomScrollView(
controller: controller,
slivers: [
MainSliverAppBar(
widescreen: widescreen,
setSearch: _setSearch,
searchHintText: 'Search Job Listings',
themeCallback: widget.themeCallback,
filterIconButton:
_filterIconButton(jobTypeFilters, offerTypeFilters),
updateLoggedIn: widget.updateLoggedIn,
generatePDF: _generatePDF,
),
FutureBuilder(
future: widget.refreshJobDataOverviewFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
if (snapshot.data.runtimeType == String) {
_isPreviousData = false;
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
Center(
child: Text(snapshot.data,
textAlign: TextAlign.center)),
Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Text('Retry'),
onPressed: () async {
if (!_isRetrying) {
setState(() {
_isRetrying = true;
});
await widget.updateBusinessesCallback(
null, null);
}
},
),
),
]),
));
}
overviewBusinesses = snapshot.data;
_isPreviousData = true;
return JobDisplayPanel(
jobGroupedBusinesses:
_filterBySearch(overviewBusinesses, searchQuery),
widescreen: widescreen,
);
} else if (snapshot.hasError) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Text(
'Error when loading data! Error: ${snapshot.error}'),
));
}
} else if (snapshot.connectionState ==
ConnectionState.waiting) {
if (_isPreviousData) {
return JobDisplayPanel(
jobGroupedBusinesses:
_filterBySearch(overviewBusinesses, searchQuery),
widescreen: widescreen,
);
} else {
return SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: const SizedBox(
width: 75,
height: 75,
child: RiveAnimation.asset(
'assets/mdev_triangle_loading.riv'),
),
));
}
}
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'\nError: ${snapshot.error}',
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
),
);
}),
],
),
);
}
Widget _filterIconButton(
Set<JobType> jobTypeFilters, Set<OfferType> offerTypeFilters) {
Set<JobType> selectedJobTypeChips = Set.from(jobTypeFilters);
Set<OfferType> selectedOfferTypeChips = Set.from(offerTypeFilters);
return IconButton(
icon: Icon(
Icons.filter_list,
color: jobTypeFilters.isNotEmpty
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
onPressed: () {
selectedJobTypeChips = Set.from(jobTypeFilters);
selectedOfferTypeChips = Set.from(offerTypeFilters);
showDialog(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
void setDialogState(Set<JobType>? newJobTypeFilters,
Set<OfferType>? newOfferTypeFilters) {
if (newJobTypeFilters != null) {
setState(() {
selectedJobTypeChips = newJobTypeFilters;
});
}
if (newOfferTypeFilters != null) {
setState(() {
selectedOfferTypeChips = newOfferTypeFilters;
});
}
}
List<Widget> jobTypeChips = [];
for (JobType type in JobType.values) {
jobTypeChips.add(FilterChip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
showCheckmark: false,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color:
Theme.of(context).colorScheme.secondary)),
selectedColor: Theme.of(context).colorScheme.secondary,
label: Text(
getNameFromJobType(type),
style: TextStyle(
color: selectedJobTypeChips.contains(type)
? Theme.of(context).colorScheme.onSecondary
: Theme.of(context).colorScheme.onSurface),
),
selected: selectedJobTypeChips.contains(type),
onSelected: (bool selected) {
if (selected) {
selectedJobTypeChips.add(type);
} else {
selectedJobTypeChips.remove(type);
}
setDialogState(selectedJobTypeChips, null);
}));
}
List<Widget> offerTypeChips = [];
for (OfferType type in OfferType.values) {
offerTypeChips.add(FilterChip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
showCheckmark: false,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color:
Theme.of(context).colorScheme.secondary)),
selectedColor: Theme.of(context).colorScheme.secondary,
label: Text(
getNameFromOfferType(type),
style: TextStyle(
color: selectedOfferTypeChips.contains(type)
? Theme.of(context).colorScheme.onSecondary
: Theme.of(context).colorScheme.onSurface),
),
selected: selectedOfferTypeChips.contains(type),
onSelected: (bool selected) {
if (selected) {
selectedOfferTypeChips.add(type);
} else {
selectedOfferTypeChips.remove(type);
}
setDialogState(null, selectedOfferTypeChips);
}));
}
return AlertDialog(
title: const Text('Filter Options'),
content: SizedBox(
width: 400,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Job Type Filters:'),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: jobTypeChips,
),
),
const Text('Offer Type Filters:'),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: offerTypeChips,
),
),
],
),
),
actions: [
TextButton(
child: const Text('Reset'),
onPressed: () {
_setFilters(<JobType>{}, <OfferType>{});
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Cancel'),
onPressed: () {
// setDialogState(jobTypeFilters, offerTypeFilters);
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Apply'),
onPressed: () {
_setFilters(
selectedJobTypeChips, selectedOfferTypeChips);
Navigator.of(context).pop();
},
)
],
);
});
});
});
}
Widget? _getFAB(bool widescreen) {
if (!widescreen && loggedIn) {
return FloatingActionButton.extended(
extendedIconLabelSpacing: _extended ? 8.0 : 0,
extendedPadding: const EdgeInsets.symmetric(horizontal: 16),
icon: const Icon(Icons.add),
label: AnimatedSize(
curve: Easing.standard,
duration: const Duration(milliseconds: 300),
child: _extended ? const Text('Add Job Listing') : Container(),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CreateEditJobListing()));
},
);
}
return null;
}
}
class JobDisplayPanel extends StatefulWidget {
final Map<JobType, List<Business>> jobGroupedBusinesses;
final bool widescreen;
const JobDisplayPanel({
super.key,
required this.jobGroupedBusinesses,
required this.widescreen,
});
@override
State<JobDisplayPanel> createState() => _JobDisplayPanelState();
}
class _JobDisplayPanelState extends State<JobDisplayPanel> {
@override
Widget build(BuildContext context) {
if (widget.jobGroupedBusinesses.keys.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'No results found!\nPlease change your search filters.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
),
),
);
}
List<_JobHeader> headers = [];
for (JobType jobType in widget.jobGroupedBusinesses.keys) {
headers.add(_JobHeader(
jobType: jobType,
widescreen: widget.widescreen,
businesses: widget.jobGroupedBusinesses[jobType]!));
}
headers.sort((a, b) => a.jobType.index.compareTo(b.jobType.index));
return MultiSliver(children: headers);
}
}
class _JobHeader extends StatefulWidget {
final JobType jobType;
final List<Business> businesses;
final bool widescreen;
const _JobHeader({
required this.jobType,
required this.businesses,
required this.widescreen,
});
@override
State<_JobHeader> createState() => _JobHeaderState();
}
class _JobHeaderState extends State<_JobHeader> {
@override
Widget build(BuildContext context) {
return SliverStickyHeader(
header: Container(
height: 55.0,
color: Theme.of(context).colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
alignment: Alignment.centerLeft,
child: _getHeaderRow(),
),
sliver: _getChildSliver(widget.businesses, widget.widescreen),
);
}
Widget _getHeaderRow() {
return Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12.0),
child: Icon(
getIconFromJobType(widget.jobType),
color: Theme.of(context).colorScheme.onPrimary,
)),
Text(getNameFromJobType(widget.jobType),
style: TextStyle(color: Theme.of(context).colorScheme.onPrimary)),
],
);
}
Widget _getChildSliver(List<Business> businesses, bool widescreen) {
if (widescreen) {
return SliverPadding(
padding: const EdgeInsets.all(4),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
mainAxisExtent: 250.0,
maxCrossAxisExtent: 400.0,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
),
delegate: SliverChildBuilderDelegate(
childCount: businesses.length,
(BuildContext context, int index) {
return _jobBusinessTile(
businesses[index],
widget.jobType,
);
},
),
),
);
} else {
return SliverList(
delegate: SliverChildBuilderDelegate(
childCount: businesses.length,
(BuildContext context, int index) {
return _jobBusinessListItem(
businesses[index],
widget.jobType,
);
},
),
);
}
}
/// A desktop widget that displays basic info about a job
Widget _jobBusinessTile(Business business, JobType jobType) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => JobListingDetail(
listing: business.listings![0],
fromBusiness: business,
)));
},
child: Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Badge(
label: Text(
getLetterFromOfferType(
business.listings![0].offerType!),
style: const TextStyle(fontSize: 16),
),
largeSize: 24,
offset: const Offset(12, -3),
textColor: Colors.white,
backgroundColor: getColorFromOfferType(
business.listings![0].offerType!),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network(
'$apiAddress/logos/${business.id}',
height: 48,
width: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(
getIconFromJobType(business.listings![0].type!),
size: 48);
}),
),
)),
Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${business.listings![0].name} (${getNameFromOfferType(business.listings![0].offerType!)})',
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
business.listings![0].description,
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (business.listings![0].link != null &&
business.listings![0].link!.isNotEmpty)
IconButton(
icon: const Icon(Icons.link),
onPressed: () {
launchUrl(Uri.parse(business.listings![0].link!));
},
),
IconButton(
icon: const Icon(Icons.location_on),
onPressed: () {
launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
},
),
if (business.contactPhone != null)
IconButton(
icon: const Icon(Icons.phone),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.surface,
title:
Text('Contact ${business.contactName}'),
content: Text(
'Would you like to call or text ${business.contactName}?'),
actions: [
TextButton(
child: const Text('Text'),
onPressed: () {
launchUrl(Uri.parse(
'sms:${business.contactPhone}'));
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Call'),
onPressed: () async {
launchUrl(Uri.parse(
'tel:${business.contactPhone}'));
Navigator.of(context).pop();
}),
],
);
});
},
),
if (business.contactEmail != null)
IconButton(
icon: const Icon(Icons.email),
onPressed: () {
launchUrl(
Uri.parse('mailto:${business.contactEmail}'));
},
),
],
)),
],
),
),
),
);
}
/// A mobile widget that displays basic info about a job
Widget _jobBusinessListItem(Business business, JobType? jobType) {
return Card(
child: ListTile(
leading: Badge(
label: Text(getLetterFromOfferType(business.listings![0].offerType!)),
textColor: Colors.white,
isLabelVisible: true,
offset: const Offset(7, -5),
alignment: Alignment.topRight,
padding: business.listings![0].offerType! == OfferType.internship
? const EdgeInsets.symmetric(horizontal: 5)
: null,
backgroundColor:
getColorFromOfferType(business.listings![0].offerType!),
child: ClipRRect(
borderRadius: BorderRadius.circular(3.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 24, width: 24, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(
getIconFromJobType(business.listings![0].type!),
);
})),
),
title: Text(
'${business.listings![0].name} (${getNameFromOfferType(business.listings![0].offerType!)})'),
subtitle: Text(business.listings![0].description,
maxLines: 2, overflow: TextOverflow.ellipsis),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => JobListingDetail(
listing: business.listings![0],
fromBusiness: business,
)));
},
),
);
}
}
+86 -38
View File
@@ -1,13 +1,10 @@
import 'package:fbla_ui/api_logic.dart'; import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/home.dart'; import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
bool loggedIn = false;
class SignInPage extends StatefulWidget { class SignInPage extends StatefulWidget {
final Callback refreshAccount; final void Function(bool) refreshAccount;
const SignInPage({super.key, required this.refreshAccount}); const SignInPage({super.key, required this.refreshAccount});
@@ -40,7 +37,7 @@ class _SignInPageState extends State<SignInPage> {
heightFactor: 1.0, heightFactor: 1.0,
child: Container( child: Container(
padding: const EdgeInsets.fromLTRB(12, 50, 12, 50), padding: const EdgeInsets.fromLTRB(12, 50, 12, 50),
height: 475, height: 450,
width: 500, width: 500,
child: Card( child: Card(
child: Padding( child: Padding(
@@ -63,9 +60,12 @@ class _SignInPageState extends State<SignInPage> {
controller: _usernameController, controller: _usernameController,
autocorrect: false, autocorrect: false,
decoration: const InputDecoration( decoration: const InputDecoration(
prefixIcon: Icon(Icons.person_outline), prefixIcon: Padding(
padding: EdgeInsets.all(16.0),
child: Icon(Icons.person_outline),
),
labelText: 'Username', labelText: 'Username',
border: OutlineInputBorder()), ),
), ),
), ),
Padding( Padding(
@@ -97,8 +97,7 @@ class _SignInPageState extends State<SignInPage> {
await prefs.setString('username', username); await prefs.setString('username', username);
await prefs.setString('password', password); await prefs.setString('password', password);
await prefs.setBool('rememberMe', rememberMe); await prefs.setBool('rememberMe', rememberMe);
loggedIn = true; widget.refreshAccount(true);
widget.refreshAccount();
Navigator.of(context).pop(); Navigator.of(context).pop();
} else { } else {
setState(() { setState(() {
@@ -111,11 +110,13 @@ class _SignInPageState extends State<SignInPage> {
autocorrect: false, autocorrect: false,
obscureText: obscurePassword, obscureText: obscurePassword,
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(Icons.fingerprint), prefixIcon: const Padding(
padding: EdgeInsets.all(16.0),
child: Icon(Icons.fingerprint),
),
labelText: 'Password', labelText: 'Password',
border: const OutlineInputBorder(),
suffixIcon: Padding( suffixIcon: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.only(right: 16.0),
child: IconButton( child: IconButton(
onPressed: () { onPressed: () {
setState(() { setState(() {
@@ -135,35 +136,45 @@ class _SignInPageState extends State<SignInPage> {
errorMessage!, errorMessage!,
style: const TextStyle(color: Colors.red), style: const TextStyle(color: Colors.red),
), ),
CheckboxListTile( Padding(
value: rememberMe, padding: const EdgeInsets.only(
onChanged: (value) async { top: 8.0, left: 8.0, right: 8.0),
setState(() { child: FilledButton(
rememberMe = value!; style: FilledButton.styleFrom(
}); shape: const RoundedRectangleBorder(
}, borderRadius:
title: const Text('Remember me'), BorderRadius.all(Radius.circular(6)))),
), child: SizedBox(
ElevatedButton.icon( width: 374,
style: ElevatedButton.styleFrom( height: 40,
backgroundColor: child: Row(
Theme.of(context).colorScheme.primary, mainAxisSize: MainAxisSize.min,
// padding: const EdgeInsets.only(left: 20.0, right: 20.0, top: 12.0, bottom: 12.0), mainAxisAlignment: MainAxisAlignment.center,
), children: [
icon: _isloading _isloading
? const SizedBox( ? const Padding(
padding: EdgeInsets.only(right: 8.0),
child: SizedBox(
width: 20, width: 20,
height: 20, height: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: Colors.white, color: Colors.white,
strokeWidth: 3, strokeWidth: 3,
)) )),
: const Icon(Icons.done, color: Colors.white), )
label: const Text('Sign In', : const Padding(
style: TextStyle(color: Colors.white)), padding: EdgeInsets.only(right: 8.0),
child: Icon(Icons.done,
color: Colors.white),
),
const Text('Sign in',
style: TextStyle(
color: Colors.white, fontSize: 18)),
],
),
),
onPressed: () async { onPressed: () async {
setState(() { setState(() {
errorMessage = null;
_isloading = true; _isloading = true;
}); });
jwt = await signIn(username, password).timeout( jwt = await signIn(username, password).timeout(
@@ -183,8 +194,7 @@ class _SignInPageState extends State<SignInPage> {
await prefs.setString('username', username); await prefs.setString('username', username);
await prefs.setString('password', password); await prefs.setString('password', password);
await prefs.setBool('rememberMe', rememberMe); await prefs.setBool('rememberMe', rememberMe);
loggedIn = true; widget.refreshAccount(true);
widget.refreshAccount();
Navigator.of(context).pop(); Navigator.of(context).pop();
} else { } else {
setState(() { setState(() {
@@ -194,6 +204,44 @@ class _SignInPageState extends State<SignInPage> {
} }
}, },
), ),
),
Expanded(
child: Align(
alignment: Alignment.bottomLeft,
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
child: Row(
children: [
Checkbox(
value: rememberMe,
onChanged: (value) {
setState(() {
rememberMe = value!;
});
}),
const Padding(
padding: EdgeInsets.all(8.0),
child: Text('Remember me'),
)
],
),
onTap: () {
setState(() {
rememberMe = !rememberMe;
});
},
),
),
),
],
),
),
),
], ],
), ),
), ),
-872
View File
@@ -1,872 +0,0 @@
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/pages/business_detail.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:sliver_tools/sliver_tools.dart';
import 'package:url_launcher/url_launcher.dart';
late String jwt;
Set<BusinessType> filters = <BusinessType>{};
Set<BusinessType> selectedChips = <BusinessType>{};
String searchFilter = '';
bool isFiltered = false;
Set<Business> selectedBusinesses = <Business>{};
Set<DataType> selectedDataTypes = <DataType>{};
Set<DataType> dataTypeFilters = <DataType>{};
enum DataType {
logo,
name,
description,
// type,
website,
contactName,
contactEmail,
contactPhone,
notes,
}
Map<DataType, int> dataTypeValues = {
DataType.logo: 0,
DataType.name: 1,
DataType.description: 2,
// DataType.type: 3,
DataType.website: 4,
DataType.contactName: 5,
DataType.contactEmail: 6,
DataType.contactPhone: 7,
DataType.notes: 8
};
Map<DataType, String> dataTypeFriendly = {
DataType.logo: 'Logo',
DataType.name: 'Name',
DataType.description: 'Description',
// DataType.type: 'Type',
DataType.website: 'Website',
DataType.contactName: 'Contact Name',
DataType.contactEmail: 'Contact Email',
DataType.contactPhone: 'Contact Phone',
DataType.notes: 'Notes'
};
Set<DataType> sortDataTypes(Set<DataType> set) {
List<DataType> list = set.toList();
list.sort((a, b) {
return dataTypeValues[a]!.compareTo(dataTypeValues[b]!);
});
set = list.toSet();
return set;
}
enum BusinessType {
food,
shop,
outdoors,
manufacturing,
entertainment,
other,
}
enum JobType { cashier, server, mechanic, other }
class JobListing {
String? id;
String? businessId;
String name;
String description;
JobType type;
String? wage;
String? link;
JobListing(
{this.id,
this.businessId,
required this.name,
required this.description,
required this.type,
this.wage,
this.link});
}
class Business {
int id;
String name;
String description;
String? website;
String? contactName;
String? contactEmail;
String? contactPhone;
String? notes;
String? locationName;
String? locationAddress;
List<JobListing>? listings;
Business(
{required this.id,
required this.name,
required this.description,
this.website,
this.contactName,
this.contactEmail,
this.contactPhone,
this.notes,
this.locationName,
this.locationAddress,
this.listings});
factory Business.fromJson(Map<String, dynamic> json) {
List<JobListing>? listings = [];
for (int i = 0; i < json['listings'].length; i++) {
listings.add(JobListing(
name: json['listings']['name'],
description: json['listings']['description'],
type: json['listings']['type'],
wage: json['listings']['wage'],
link: json['listings']['link']));
}
return Business(
id: json['id'],
name: json['name'],
description: json['description'],
website: json['website'],
contactName: json['contactName'],
contactEmail: json['contactEmail'],
contactPhone: json['contactPhone'],
notes: json['notes'],
locationName: json['locationName'],
locationAddress: json['locationAddress'],
listings: listings);
}
factory Business.copy(Business input) {
return Business(
id: input.id,
name: input.name,
description: input.description,
website: input.website,
contactName: input.contactName,
contactEmail: input.contactEmail,
contactPhone: input.contactPhone,
notes: input.notes,
locationName: input.locationName,
locationAddress: input.locationAddress,
listings: input.listings);
}
}
// Map<BusinessType, List<Business>> groupBusinesses(List<Business> businesses) {
// Map<BusinessType, List<Business>> groupedBusinesses =
// groupBy<Business, BusinessType>(businesses, (business) => business.type!);
//
// return groupedBusinesses;
// }
Icon getIconFromType(BusinessType type, double size, Color color) {
switch (type) {
case BusinessType.food:
return Icon(
Icons.restaurant,
size: size,
color: color,
);
case BusinessType.shop:
return Icon(
Icons.store,
size: size,
color: color,
);
case BusinessType.outdoors:
return Icon(
Icons.forest,
size: size,
color: color,
);
case BusinessType.manufacturing:
return Icon(
Icons.factory,
size: size,
color: color,
);
case BusinessType.entertainment:
return Icon(
Icons.live_tv,
size: size,
color: color,
);
case BusinessType.other:
return Icon(
Icons.business,
size: size,
color: color,
);
}
}
Icon getIconFromJobType(JobType type, double size, Color color) {
switch (type) {
case JobType.cashier:
return Icon(
Icons.shopping_bag,
size: size,
color: color,
);
case JobType.server:
return Icon(
Icons.restaurant,
size: size,
color: color,
);
case JobType.mechanic:
return Icon(
Icons.construction,
size: size,
color: color,
);
case JobType.other:
return Icon(
Icons.work,
size: size,
color: color,
);
}
}
pw.Icon getPwIconFromType(BusinessType type, double size, PdfColor color) {
switch (type) {
case BusinessType.food:
return pw.Icon(const pw.IconData(0xe56c), size: size, color: color);
case BusinessType.shop:
return pw.Icon(const pw.IconData(0xea12), size: size, color: color);
case BusinessType.outdoors:
return pw.Icon(const pw.IconData(0xea99), size: size, color: color);
case BusinessType.manufacturing:
return pw.Icon(const pw.IconData(0xebbc), size: size, color: color);
case BusinessType.entertainment:
return pw.Icon(const pw.IconData(0xe639), size: size, color: color);
case BusinessType.other:
return pw.Icon(const pw.IconData(0xe0af), size: size, color: color);
}
}
pw.Icon getPwIconFromJobType(JobType type, double size, PdfColor color) {
switch (type) {
case JobType.cashier:
return pw.Icon(const pw.IconData(0xf1cc), size: size, color: color);
case JobType.server:
return pw.Icon(const pw.IconData(0xe56c), size: size, color: color);
case JobType.mechanic:
return pw.Icon(const pw.IconData(0xea3c), size: size, color: color);
case JobType.other:
return pw.Icon(const pw.IconData(0xe8f9), size: size, color: color);
}
}
Text getNameFromType(BusinessType type, Color color) {
switch (type) {
case BusinessType.food:
return Text('Food Related', style: TextStyle(color: color));
case BusinessType.shop:
return Text('Shops', style: TextStyle(color: color));
case BusinessType.outdoors:
return Text('Outdoors', style: TextStyle(color: color));
case BusinessType.manufacturing:
return Text('Manufacturing', style: TextStyle(color: color));
case BusinessType.entertainment:
return Text('Entertainment', style: TextStyle(color: color));
case BusinessType.other:
return Text('Other', style: TextStyle(color: color));
}
}
Text getNameFromJobType(JobType type, Color color) {
switch (type) {
case JobType.cashier:
return Text('Cashier', style: TextStyle(color: color));
case JobType.server:
return Text('Server', style: TextStyle(color: color));
case JobType.mechanic:
return Text('Mechanic', style: TextStyle(color: color));
case JobType.other:
return Text('Other', style: TextStyle(color: color));
}
}
Icon getIconFromThemeMode(ThemeMode theme) {
switch (theme) {
case ThemeMode.dark:
return const Icon(Icons.dark_mode);
case ThemeMode.light:
return const Icon(Icons.light_mode);
case ThemeMode.system:
return const Icon(Icons.brightness_4);
}
}
class BusinessDisplayPanel extends StatefulWidget {
final Map<JobType, List<Business>> groupedBusinesses;
final bool widescreen;
final bool selectable;
const BusinessDisplayPanel(
{super.key,
required this.groupedBusinesses,
required this.widescreen,
required this.selectable});
@override
State<BusinessDisplayPanel> createState() => _BusinessDisplayPanelState();
}
class _BusinessDisplayPanelState extends State<BusinessDisplayPanel> {
Set<Business> selectedBusinesses = <Business>{};
@override
Widget build(BuildContext context) {
List<BusinessHeader> headers = [];
// List<Business> filteredBusinesses = [];
// for (var business in widget.groupedBusinesses.) {
// if (business.name.toLowerCase().contains(searchFilter.toLowerCase())) {
// filteredBusinesses.add(business);
// }
// }
if (filters.isNotEmpty) {
isFiltered = true;
}
// for (var i = 0; i < businessTypes.length; i++) {
// if (filters.contains(businessTypes[i])) {
// isFiltered = true;
// }
// }
if (isFiltered) {
for (JobType jobType in widget.groupedBusinesses.keys) {
if (filters.contains(jobType)) {
headers.add(BusinessHeader(
type: jobType,
widescreen: widget.widescreen,
selectable: widget.selectable,
selectedBusinesses: selectedBusinesses,
businesses: widget.groupedBusinesses[jobType]!));
}
}
} else {
for (JobType jobType in widget.groupedBusinesses.keys) {
headers.add(BusinessHeader(
type: jobType,
widescreen: widget.widescreen,
selectable: widget.selectable,
selectedBusinesses: selectedBusinesses,
businesses: widget.groupedBusinesses[jobType]!));
}
}
headers.sort((a, b) => a.type.index.compareTo(b.type.index));
return MultiSliver(children: headers);
}
}
class BusinessHeader extends StatefulWidget {
final JobType type;
final List<Business> businesses;
final Set<Business> selectedBusinesses;
final bool widescreen;
final bool selectable;
const BusinessHeader({
super.key,
required this.type,
required this.businesses,
required this.selectedBusinesses,
required this.widescreen,
required this.selectable,
});
@override
State<BusinessHeader> createState() => _BusinessHeaderState();
}
class _BusinessHeaderState extends State<BusinessHeader> {
refresh() {
setState(() {});
}
@override
Widget build(BuildContext context) {
return SliverStickyHeader(
header: Container(
height: 55.0,
color: Theme.of(context).colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
alignment: Alignment.centerLeft,
child: _getHeaderRow(widget.selectable),
),
sliver: _getChildSliver(
widget.businesses, widget.widescreen, widget.selectable),
);
}
Widget _getHeaderRow(bool selectable) {
if (selectable) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12.0),
child: getIconFromJobType(
widget.type, 24, Theme.of(context).colorScheme.onPrimary),
),
getNameFromJobType(
widget.type, Theme.of(context).colorScheme.onPrimary),
],
),
Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Checkbox(
checkColor: Theme.of(context).colorScheme.primary,
activeColor: Theme.of(context).colorScheme.onPrimary,
value: selectedBusinesses.containsAll(widget.businesses),
onChanged: (value) {
if (value!) {
setState(() {
selectedBusinesses.addAll(widget.businesses);
});
} else {
setState(() {
selectedBusinesses.removeAll(widget.businesses);
});
}
},
),
),
],
);
} else {
return Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12.0),
child: getIconFromJobType(
widget.type, 24, Theme.of(context).colorScheme.onPrimary),
),
getNameFromJobType(
widget.type, Theme.of(context).colorScheme.onPrimary),
],
);
}
}
Widget _getChildSliver(
List<Business> businesses, bool widescreen, bool selectable) {
if (widescreen) {
return SliverGrid(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
mainAxisExtent: 250.0,
maxCrossAxisExtent: 400.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
// childAspectRatio: 4.0,
),
delegate: SliverChildBuilderDelegate(
childCount: businesses.length,
(BuildContext context, int index) {
return BusinessCard(
business: businesses[index],
selectable: selectable,
widescreen: widescreen,
callback: refresh,
type: widget.type,
);
},
),
);
} else {
return SliverList(
delegate: SliverChildBuilderDelegate(
childCount: businesses.length,
(BuildContext context, int index) {
return BusinessCard(
business: businesses[index],
selectable: selectable,
widescreen: widescreen,
callback: refresh,
type: widget.type,
);
},
),
);
}
}
}
class BusinessCard extends StatefulWidget {
final Business business;
final bool widescreen;
final bool selectable;
final Function callback;
final JobType type;
const BusinessCard(
{super.key,
required this.business,
required this.widescreen,
required this.selectable,
required this.callback,
required this.type});
@override
State<BusinessCard> createState() => _BusinessCardState();
}
class _BusinessCardState extends State<BusinessCard> {
@override
Widget build(BuildContext context) {
if (widget.widescreen) {
return _businessTile(widget.business, widget.selectable, widget.type);
} else {
return _businessListItem(
widget.business, widget.selectable, widget.callback, widget.type);
}
}
Widget _businessTile(Business business, bool selectable, JobType type) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => BusinessDetail(
id: business.id,
name: business.name,
clickFromType: type,
)));
},
child: Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_getTileRow(business, selectable, widget.callback, type),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
business.description,
maxLines: selectable ? 7 : 5,
overflow: TextOverflow.ellipsis,
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: !selectable
? Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.link),
onPressed: () {
launchUrl(
Uri.parse('https://${business.website}'));
},
),
if ((business.locationName != null) &&
(business.locationName != ''))
IconButton(
icon: const Icon(Icons.location_on),
onPressed: () {
launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
},
),
if ((business.contactPhone != null) &&
(business.contactPhone != ''))
IconButton(
icon: const Icon(Icons.phone),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Theme.of(context)
.colorScheme
.background,
title: Text((business.contactName ==
null ||
business.contactName == '')
? 'Contact ${business.name}?'
: 'Contact ${business.contactName}'),
content: Text((business.contactName ==
null ||
business.contactName == '')
? 'Would you like to call or text ${business.name}?'
: 'Would you like to call or text ${business.contactName}?'),
actions: [
TextButton(
child: const Text('Text'),
onPressed: () {
launchUrl(Uri.parse(
'sms:${business.contactPhone}'));
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Call'),
onPressed: () async {
launchUrl(Uri.parse(
'tel:${business.contactPhone}'));
Navigator.of(context).pop();
}),
],
);
});
},
),
if ((business.contactEmail != null) &&
(business.contactEmail != ''))
IconButton(
icon: const Icon(Icons.email),
onPressed: () {
launchUrl(Uri.parse(
'mailto:${business.contactEmail}'));
},
),
],
)
: null),
],
),
),
),
);
}
Widget _getTileRow(
Business business, bool selectable, Function callback, JobType type) {
if (selectable) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 48, width: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromJobType(
type, 48, Theme.of(context).colorScheme.onSurface);
}),
),
),
Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
business.name,
style:
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
Padding(
padding: const EdgeInsets.only(right: 24.0),
child: _checkbox(callback),
)
],
);
} else {
return Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 48, width: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromJobType(
type, 48, Theme.of(context).colorScheme.onSurface);
}),
)),
Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
business.name,
style:
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
],
);
}
}
Widget _businessListItem(
Business business, bool selectable, Function callback, JobType type) {
return Card(
child: ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(3.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 24, width: 24, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromJobType(
type, 24, Theme.of(context).colorScheme.onSurface);
})),
title: Text(business.name),
subtitle: Text(business.description,
maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: _getCheckbox(selectable, callback),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => BusinessDetail(
id: business.id,
name: business.name,
clickFromType: type,
)));
},
),
);
}
Widget _checkbox(Function callback) {
return Checkbox(
value: selectedBusinesses.contains(widget.business),
onChanged: (value) {
if (value!) {
setState(() {
selectedBusinesses.add(widget.business);
});
} else {
setState(() {
selectedBusinesses.remove(widget.business);
});
}
callback();
},
);
}
Widget? _getCheckbox(bool selectable, Function callback) {
if (selectable) {
return _checkbox(callback);
} else {
return null;
}
}
}
class FilterChips extends StatefulWidget {
const FilterChips({super.key});
@override
State<FilterChips> createState() => _FilterChipsState();
}
class _FilterChipsState extends State<FilterChips> {
List<Padding> filterChips() {
List<Padding> chips = [];
for (var type in BusinessType.values) {
chips.add(Padding(
padding: const EdgeInsets.only(left: 4.0, right: 4.0),
child: FilterChip(
showCheckmark: false,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
label:
getNameFromType(type, Theme.of(context).colorScheme.onSurface),
selected: selectedChips.contains(type),
onSelected: (bool selected) {
setState(() {
if (selected) {
selectedChips.add(type);
} else {
selectedChips.remove(type);
}
});
}),
));
}
return chips;
}
@override
Widget build(BuildContext context) {
return Wrap(
children: filterChips(),
);
}
}
class FilterDataTypeChips extends StatefulWidget {
const FilterDataTypeChips({super.key});
@override
State<FilterDataTypeChips> createState() => _FilterDataTypeChipsState();
}
class _FilterDataTypeChipsState extends State<FilterDataTypeChips> {
List<Padding> filterDataTypeChips() {
List<Padding> chips = [];
for (var type in DataType.values) {
chips.add(Padding(
padding:
const EdgeInsets.only(left: 3.0, right: 3.0, bottom: 3.0, top: 3.0),
// child: ActionChip(
// avatar: selectedDataTypes.contains(type) ? Icon(Icons.check_box) : Icon(Icons.check_box_outline_blank),
// label: Text(type.name),
// onPressed: () {
// if (!selectedDataTypes.contains(type)) {
// setState(() {
// selectedDataTypes.add(type);
// });
// } else {
// setState(() {
// selectedDataTypes.remove(type);
// });
// }
// },
// ),
child: FilterChip(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side:
BorderSide(color: Theme.of(context).colorScheme.secondary)),
label: Text(dataTypeFriendly[type]!),
showCheckmark: false,
selected: selectedDataTypes.contains(type),
onSelected: (bool selected) {
setState(() {
if (selected) {
selectedDataTypes.add(type);
} else {
selectedDataTypes.remove(type);
}
});
}),
));
}
return chips;
}
@override
Widget build(BuildContext context) {
return Wrap(
children: filterDataTypeChips(),
);
}
}
+399
View File
@@ -0,0 +1,399 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:http/http.dart' as http;
var apiAddress = 'https://homelab.marinodev.com/fbla-api';
// var apiAddress = 'http://192.168.0.114:8000/fbla-api'; // TODO
var client = http.Client();
Future fetchBusinessData() async {
try {
var response = await http
.get(Uri.parse('$apiAddress/businessdata'))
.timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
var decodedResponse = json.decode(response.body);
List<Business> businessList = List<Business>.from(
decodedResponse.map((json) => Business.fromJson(json)).toList());
return businessList;
} else {
return 'Error ${response.statusCode}! Please try again later!';
}
} on TimeoutException {
return 'Unable to connect to server (timeout).\nPlease try again later.';
} on SocketException {
return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n';
}
}
Future fetchBusinessNames() async {
try {
var response = await http
.get(Uri.parse('$apiAddress/businessdata/businessnames'))
.timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
List<Map<String, dynamic>> decodedResponse =
json.decode(response.body).cast<Map<String, dynamic>>();
return decodedResponse;
} else {
return 'Error ${response.statusCode}! Please try again later!';
}
} on TimeoutException {
return 'Unable to connect to server (timeout).\nPlease try again later.';
} on SocketException {
return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n';
}
}
Future fetchBusinessDataOverviewJobs(
{Iterable<JobType>? typeFilters, Iterable<OfferType>? offerFilters}) async {
try {
String uriString = '$apiAddress/businessdata/overview/jobs';
if (typeFilters != null && typeFilters.isNotEmpty) {
uriString +=
'?typeFilters=${typeFilters.map((jobType) => jobType.name).join(',')}';
if (offerFilters != null && offerFilters.isNotEmpty) {
uriString +=
'&offerFilters=${offerFilters.map((offerType) => offerType.name).join(',')}';
}
} else if (offerFilters != null && offerFilters.isNotEmpty) {
uriString +=
'?offerFilters=${offerFilters.map((offerType) => offerType.name).join(',')}';
}
Uri uri = Uri.parse(uriString);
var response = await http.get(uri).timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
List<Map<String, dynamic>> decodedResponse =
json.decode(response.body).cast<Map<String, dynamic>>();
List<Business> initialBusinesses =
decodedResponse.map((element) => Business.fromJson(element)).toList();
Map<JobType, List<Business>> groupedBusinesses = {};
for (Business business in initialBusinesses) {
for (JobListing job in business.listings!) {
List<Business> newBusinesses = groupedBusinesses[job.type!] ?? [];
Business newBusiness = Business.copy(business);
newBusiness.listings =
newBusiness.listings!.where((element) => element == job).toList();
newBusinesses.add(newBusiness);
groupedBusinesses.addAll({job.type!: newBusinesses});
}
}
return groupedBusinesses;
} else {
return 'Error ${response.statusCode}! Please try again later!';
}
} on TimeoutException {
return 'Unable to connect to server (timeout).\nPlease try again later.';
} on SocketException {
return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n';
}
}
Future fetchBusinessDataOverviewTypes({List<BusinessType>? typeFilters}) async {
try {
String? typeString =
typeFilters?.map((jobType) => jobType.name).toList().join(',');
Uri uri = Uri.parse(
'$apiAddress/businessdata/overview/types?filters=$typeString');
if (typeFilters == null || typeFilters.isEmpty) {
uri = Uri.parse('$apiAddress/businessdata/overview/types');
}
var response = await http.get(uri).timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
var decodedResponse = json.decode(response.body);
Map<BusinessType, List<Business>> groupedBusinesses = {};
for (String stringType in decodedResponse.keys) {
List<Business> businesses = [];
for (Map<String, dynamic> map in decodedResponse[stringType]) {
map.addAll({'type': stringType});
Business business = Business.fromJson(map);
businesses.add(business);
}
groupedBusinesses
.addAll({BusinessType.values.byName(stringType): businesses});
}
return groupedBusinesses;
} else {
return 'Error ${response.statusCode}! Please try again later!';
}
} on TimeoutException {
return 'Unable to connect to server (timeout).\nPlease try again later.';
} on SocketException {
return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n';
}
}
Future fetchBusinesses(List<int> ids) async {
try {
var response = await http
.get(Uri.parse(
'$apiAddress/businessdata/businesses?businesses=${ids.join(',')}'))
.timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
List<dynamic> decodedResponse = json.decode(response.body);
List<Business> businesses = decodedResponse
.map<Business>((json) => Business.fromJson(json))
.toList();
return businesses;
} else {
return 'Error ${response.statusCode}! Please try again later!';
}
} on TimeoutException {
return 'Unable to connect to server (timeout).\nPlease try again later.';
} on SocketException {
return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n';
}
}
Future fetchBusiness(int id) async {
try {
var response = await http
.get(Uri.parse('$apiAddress/businessdata/business/$id'))
.timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
var decodedResponse = json.decode(response.body);
Business business = Business.fromJson(decodedResponse);
return business;
} else {
return 'Error ${response.statusCode}! Please try again later!';
}
} on TimeoutException {
return 'Unable to connect to server (timeout).\nPlease try again later.';
} on SocketException {
return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n';
}
}
Future fetchJob(int id) async {
try {
var response = await http
.get(Uri.parse('$apiAddress/businessdata/jobs/$id'))
.timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
var decodedResponse = json.decode(response.body);
Business business = Business.fromJson(decodedResponse);
return business;
} else {
return 'Error ${response.statusCode}! Please try again later!';
}
} on TimeoutException {
return 'Unable to connect to server (timeout).\nPlease try again later.';
} on SocketException {
return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n';
}
}
Future createBusiness(Business business) async {
var json = '''
{
"id": ${business.id},
"name": "${business.name}",
"description": "${business.description?.replaceAll('\n', '\\n')}",
"website": "${business.website}",
"type": "${business.type!.name}",
"contactName": "${business.contactName}",
"contactEmail": "${business.contactEmail}",
"contactPhone": "${business.contactPhone}",
"notes": "${business.notes}",
"locationName": "${business.locationName}",
"locationAddress": "${business.locationAddress}"
}
''';
try {
var response = await http.post(Uri.parse('$apiAddress/createbusiness'),
body: json,
headers: {'Authorization': jwt}).timeout(const Duration(seconds: 20));
if (response.statusCode != 200) {
return response.body;
}
} on TimeoutException {
return 'Unable to connect to server (timeout). Please try again later';
} on SocketException {
return 'Unable to connect to server (socket exception). Please check your internet connection.';
}
}
Future createListing(JobListing listing) async {
var json = '''
{
"id": ${listing.id},
"businessId": ${listing.businessId},
"name": "${listing.name}",
"description": "${listing.description.replaceAll('\n', '\\n')}",
"type": "${listing.type!.name}",
"offerType": "${listing.offerType!.name}",
"wage": "${listing.wage}",
"link": "${listing.link}"
}
''';
try {
var response = await http.post(Uri.parse('$apiAddress/createlisting'),
body: json,
headers: {'Authorization': jwt}).timeout(const Duration(seconds: 20));
if (response.statusCode != 200) {
return response.body;
}
} on TimeoutException {
return 'Unable to connect to server (timeout). Please try again later';
} on SocketException {
return 'Unable to connect to server (socket exception). Please check your internet connection.';
}
}
Future deleteBusiness(int id) async {
var json = '''
{
"id": $id
}
''';
try {
var response = await http.post(Uri.parse('$apiAddress/deletebusiness'),
body: json,
headers: {'Authorization': jwt}).timeout(const Duration(seconds: 20));
if (response.statusCode != 200) {
return response.body;
}
} on TimeoutException {
return 'Unable to connect to server (timeout). Please try again later';
} on SocketException {
return 'Unable to connect to server (socket exception). Please check your internet connection.';
}
}
Future deleteListing(int id) async {
var json = '''
{
"id": $id
}
''';
try {
var response = await http.post(Uri.parse('$apiAddress/deletelisting'),
body: json,
headers: {'Authorization': jwt}).timeout(const Duration(seconds: 20));
if (response.statusCode != 200) {
return response.body;
}
} on TimeoutException {
return 'Unable to connect to server (timeout). Please try again later';
} on SocketException {
return 'Unable to connect to server (socket exception). Please check your internet connection.';
}
}
Future editBusiness(Business business) async {
var json = '''
{
"id": ${business.id},
"name": "${business.name}",
"description": "${business.description?.replaceAll('\n', '\\n')}",
"website": "${business.website}",
"type": "${business.type!.name}",
"contactName": "${business.contactName}",
"contactEmail": "${business.contactEmail}",
"contactPhone": "${business.contactPhone}",
"notes": "${business.notes}",
"locationName": "${business.locationName}",
"locationAddress": "${business.locationAddress}"
}
''';
try {
var response = await http.post(Uri.parse('$apiAddress/editbusiness'),
body: json,
headers: {'Authorization': jwt}).timeout(const Duration(seconds: 20));
if (response.statusCode != 200) {
return response.body;
}
} on TimeoutException {
return 'Unable to connect to server (timeout). Please try again later';
} on SocketException {
return 'Unable to connect to server (socket exception). Please check your internet connection.';
}
}
Future editListing(JobListing listing) async {
var json = '''
{
"id": ${listing.id},
"businessId": ${listing.businessId},
"name": "${listing.name}",
"description": "${listing.description.replaceAll('\n', '\\n')}",
"type": "${listing.type!.name}",
"offerType": "${listing.offerType!.name}",
"wage": "${listing.wage}",
"link": "${listing.link}"
}
''';
try {
var response = await http.post(Uri.parse('$apiAddress/editlisting'),
body: json,
headers: {'Authorization': jwt}).timeout(const Duration(seconds: 20));
if (response.statusCode != 200) {
return response.body;
}
} on TimeoutException {
return 'Unable to connect to server (timeout). Please try again later';
} on SocketException {
return 'Unable to connect to server (socket exception). Please check your internet connection.';
}
}
Future signIn(String username, String password) async {
var json = '''
{
"username": "$username",
"password": "$password"
}
''';
var response = await http.post(
Uri.parse('$apiAddress/signin'),
body: json,
);
if (response.statusCode == 200) {
return response.body;
} else {
return 'Error: ${response.body}';
}
}
Future marinoDevLogo() async {
var response = await http.get(
Uri.parse('$apiAddress/marinodev'),
);
return response.bodyBytes;
}
Future getLogo(int logoId) async {
var response = await http.get(
Uri.parse('$apiAddress/logos/$logoId'),
);
if (response.statusCode == 200) {
return response.bodyBytes;
} else {
return 'Error ${response.statusCode}';
}
}
+595
View File
@@ -0,0 +1,595 @@
import 'dart:io';
import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:open_filex/open_filex.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
Map<DataTypeBusiness, int> dataTypePriorityBusiness = {
DataTypeBusiness.logo: 0,
DataTypeBusiness.name: 1,
DataTypeBusiness.description: 2,
DataTypeBusiness.type: 3,
DataTypeBusiness.website: 4,
DataTypeBusiness.contactName: 5,
DataTypeBusiness.contactEmail: 6,
DataTypeBusiness.contactPhone: 7,
DataTypeBusiness.notes: 8
};
Map<DataTypeBusiness, String> dataTypeFriendlyBusiness = {
DataTypeBusiness.logo: 'Logo',
DataTypeBusiness.name: 'Name',
DataTypeBusiness.description: 'Description',
DataTypeBusiness.type: 'Type',
DataTypeBusiness.website: 'Website',
DataTypeBusiness.contactName: 'Contact Name',
DataTypeBusiness.contactEmail: 'Contact Email',
DataTypeBusiness.contactPhone: 'Contact Phone',
DataTypeBusiness.notes: 'Notes'
};
Map<DataTypeJob, int> dataTypePriorityJob = {
DataTypeJob.businessName: 1,
DataTypeJob.name: 2,
DataTypeJob.description: 3,
DataTypeJob.type: 4,
DataTypeJob.offerType: 5,
DataTypeJob.wage: 6,
DataTypeJob.link: 7,
};
Map<DataTypeJob, String> dataTypeFriendlyJob = {
DataTypeJob.businessName: 'Business Name',
DataTypeJob.name: 'Job Listing Name',
DataTypeJob.description: 'Description',
DataTypeJob.type: 'Job Type',
DataTypeJob.offerType: 'Offer Type',
DataTypeJob.wage: 'Wage Information',
DataTypeJob.link: 'Additional Info Link',
};
Set<DataTypeBusiness> sortDataTypesBusiness(Set<DataTypeBusiness> set) {
List<DataTypeBusiness> list = set.toList();
list.sort((a, b) {
return dataTypePriorityBusiness[a]!.compareTo(dataTypePriorityBusiness[b]!);
});
set = list.toSet();
return set;
}
Set<DataTypeJob> sortDataTypesJob(Set<DataTypeJob> set) {
List<DataTypeJob> list = set.toList();
list.sort((a, b) {
return dataTypePriorityJob[a]!.compareTo(dataTypePriorityJob[b]!);
});
set = list.toSet();
return set;
}
class _FilterBusinessDataTypeChips extends StatefulWidget {
final Set<DataTypeBusiness> selectedDataTypesBusiness;
const _FilterBusinessDataTypeChips({required this.selectedDataTypesBusiness});
@override
State<_FilterBusinessDataTypeChips> createState() =>
_FilterBusinessDataTypeChipsState();
}
class _FilterBusinessDataTypeChipsState
extends State<_FilterBusinessDataTypeChips> {
@override
Widget build(BuildContext context) {
List<Widget> chips = [];
for (var type in DataTypeBusiness.values) {
chips.add(FilterChip(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: Theme.of(context).colorScheme.secondary)),
label: Text(dataTypeFriendlyBusiness[type]!),
showCheckmark: false,
selected: widget.selectedDataTypesBusiness.contains(type),
onSelected: (bool selected) {
setState(() {
if (selected) {
widget.selectedDataTypesBusiness.add(type);
} else {
widget.selectedDataTypesBusiness.remove(type);
}
});
}));
}
return Wrap(
spacing: 6,
runSpacing: 6,
children: chips,
);
}
}
class _FilterJobDataTypeChips extends StatefulWidget {
final Set<DataTypeJob> selectedDataTypesJob;
const _FilterJobDataTypeChips({required this.selectedDataTypesJob});
@override
State<_FilterJobDataTypeChips> createState() =>
_FilterJobDataTypeChipsState();
}
class _FilterJobDataTypeChipsState extends State<_FilterJobDataTypeChips> {
@override
Widget build(BuildContext context) {
List<Padding> chips = [];
for (var type in DataTypeJob.values) {
chips.add(Padding(
padding:
const EdgeInsets.only(left: 3.0, right: 3.0, bottom: 3.0, top: 3.0),
child: FilterChip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side:
BorderSide(color: Theme.of(context).colorScheme.secondary)),
label: Text(dataTypeFriendlyJob[type]!),
showCheckmark: false,
selected: widget.selectedDataTypesJob.contains(type),
onSelected: (bool selected) {
setState(() {
if (selected) {
widget.selectedDataTypesJob.add(type);
} else {
widget.selectedDataTypesJob.remove(type);
}
});
}),
));
}
return Wrap(
spacing: 6,
runSpacing: 6,
children: chips,
);
}
}
Future<void> generatePDF(
{required BuildContext context,
required int documentTypeIndex,
Set<Business>? selectedBusinesses,
Set<Business>? selectedJobs}) async {
List<pw.Widget> headerColumns = [];
List<pw.TableRow> tableRows = [];
Set<DataTypeBusiness> dataTypesBusiness = {};
Set<DataTypeJob> dataTypesJob = {};
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
contentPadding: const EdgeInsets.all(16),
scrollable: true,
title: const Text('Export Settings'),
content: SizedBox(
width: 400,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Text('Data columns you want to export:'),
),
documentTypeIndex == 0
? _FilterBusinessDataTypeChips(
selectedDataTypesBusiness: dataTypesBusiness,
)
: _FilterJobDataTypeChips(
selectedDataTypesJob: dataTypesJob)
],
),
),
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Generate'),
onPressed: () async {
if (documentTypeIndex == 0) {
List<Business> businesses = await fetchBusinesses(
selectedBusinesses!
.map((business) => business.id)
.toList());
if (dataTypesBusiness.isEmpty) {
dataTypesBusiness.addAll(DataTypeBusiness.values);
}
dataTypesBusiness =
sortDataTypesBusiness(dataTypesBusiness);
for (Business business in businesses) {
List<pw.Widget> businessRow = [];
if (dataTypesBusiness.contains(DataTypeBusiness.logo)) {
var apiLogo = await getLogo(business.id);
if (apiLogo.runtimeType != String) {
businessRow.add(pw.Padding(
child: pw.ClipRRect(
child: pw.Image(pw.MemoryImage(apiLogo),
height: 24, width: 24),
horizontalRadius: 4,
verticalRadius: 4),
padding: const pw.EdgeInsets.all(4.0)));
} else {
businessRow.add(pw.Padding(
child: pw.Icon(
getPwIconFromBusinessType(business.type!),
size: 24),
padding: const pw.EdgeInsets.all(4.0)));
}
}
for (DataTypeBusiness dataType in dataTypesBusiness) {
if (dataType != DataTypeBusiness.logo) {
var currentValue =
businessValueFromDataType(business, dataType);
if (currentValue != null) {
businessRow.add(pw.Padding(
child: pw.Text(businessValueFromDataType(
business, dataType)),
padding: const pw.EdgeInsets.all(4.0)));
} else {
businessRow.add(pw.Container());
}
}
}
tableRows.add(pw.TableRow(children: businessRow));
}
for (var filter in dataTypesBusiness) {
headerColumns.add(pw.Padding(
child: pw.Text(dataTypeFriendlyBusiness[filter]!,
style: const pw.TextStyle(fontSize: 10)),
padding: const pw.EdgeInsets.all(4.0)));
}
} else {
if (dataTypesJob.isEmpty) {
dataTypesJob.addAll(DataTypeJob.values);
}
dataTypesJob = sortDataTypesJob(dataTypesJob);
// List<Map<String, dynamic>> nameMapping =
// await fetchBusinessNames();
for (Business business in selectedJobs!) {
for (JobListing job in business.listings!) {
List<pw.Widget> jobRow = [];
for (DataTypeJob dataType in dataTypesJob) {
switch (dataType) {
case DataTypeJob.businessName:
jobRow.add(pw.Padding(
child: pw.Text(business.name!),
padding: const pw.EdgeInsets.all(4.0)));
case DataTypeJob.type:
jobRow.add(pw.Padding(
child: pw.Text(getNameFromJobType(job.type!)),
padding: const pw.EdgeInsets.all(4.0)));
case DataTypeJob.offerType:
jobRow.add(pw.Padding(
child: pw.Text(
getNameFromOfferType(job.offerType!)),
padding: const pw.EdgeInsets.all(4.0)));
default:
jobRow.add(pw.Padding(
child: pw.Text(
jobValueFromDataType(job, dataType) ??
''),
padding: const pw.EdgeInsets.all(4.0)));
}
// if (dataType != DataTypeJob.businessName) {
// var currentValue =
// jobValueFromDataType(job, dataType);
// if (currentValue != null) {
// jobRow.add(pw.Padding(
// child: pw.Text(currentValue),
// padding: const pw.EdgeInsets.all(4.0)));
// } else {
// jobRow.add(pw.Container());
// }
// } else {
// jobRow.add(pw.Padding(
// child: pw.Text(business.name!),
// padding: const pw.EdgeInsets.all(4.0)));
// }
}
tableRows.add(pw.TableRow(children: jobRow));
}
}
for (var filter in dataTypesJob) {
headerColumns.add(pw.Padding(
child: pw.Text(dataTypeFriendlyJob[filter]!,
style: const pw.TextStyle(fontSize: 10)),
padding: const pw.EdgeInsets.all(4.0)));
}
}
// Final Generation
DateTime dateTime = DateTime.now();
String minute = '00';
if (dateTime.minute.toString().length < 2) {
minute = '0${dateTime.minute}';
} else {
minute = dateTime.minute.toString();
}
String time = dateTime.hour <= 12
? '${dateTime.hour}:${minute}AM'
: '${dateTime.hour - 12}:${minute}PM';
String fileName =
'${documentTypeIndex == 0 ? 'Business' : 'Job Listing'} Data - ${dateTime.month}-${dateTime.day}-${dateTime.year} $time.pdf';
final pdf = pw.Document();
var svg = await rootBundle.loadString('assets/MarinoDev.svg');
var themeIcon = pw.ThemeData.withFont(
base: await PdfGoogleFonts.notoSansDisplayMedium(),
icons: await PdfGoogleFonts.materialIcons());
var finalTheme = themeIcon.copyWith(
defaultTextStyle: const pw.TextStyle(fontSize: 9),
);
pdf.addPage(pw.MultiPage(
theme: finalTheme,
pageFormat: PdfPageFormat.letter,
orientation: pw.PageOrientation.landscape,
margin: const pw.EdgeInsets.all(24),
build: (pw.Context context) {
return [
pw.Row(
mainAxisAlignment:
pw.MainAxisAlignment.spaceBetween,
children: [
pw.SvgImage(svg: svg, height: 40),
pw.Padding(
padding: const pw.EdgeInsets.all(8.0),
child: pw.Text(
'${documentTypeIndex == 0 ? 'Business' : 'Job Listing'} Datasheet',
style: pw.TextStyle(
fontSize: 32,
fontWeight: pw.FontWeight.bold)),
),
pw.Text(
'Generated on ${dateTime.month}/${dateTime.day}/${dateTime.year} at $time',
style: const pw.TextStyle(fontSize: 12),
textAlign: pw.TextAlign.right),
//
]),
pw.Table(
columnWidths: documentTypeIndex == 0
? _businessColumnSizes(dataTypesBusiness)
: _jobColumnSizes(dataTypesJob),
border: const pw.TableBorder(
bottom: pw.BorderSide(),
left: pw.BorderSide(),
right: pw.BorderSide(),
top: pw.BorderSide(),
horizontalInside: pw.BorderSide(),
verticalInside: pw.BorderSide()),
children: [
pw.TableRow(
decoration: const pw.BoxDecoration(
color: PdfColors.blue400),
children: headerColumns,
repeat: true,
),
...tableRows,
])
];
}));
Uint8List pdfBytes = await pdf.save();
if (kIsWeb) {
await Printing.sharePdf(
bytes: await pdf.save(),
filename: fileName,
);
} else {
var dir = await getTemporaryDirectory();
var tempDir = dir.path;
File pdfFile = File('$tempDir/$fileName');
pdfFile.writeAsBytesSync(pdfBytes);
OpenFilex.open(pdfFile.path);
}
Navigator.of(context).pop();
}),
],
);
});
}
Map<int, pw.TableColumnWidth> _businessColumnSizes(
Set<DataTypeBusiness> dataTypes) {
double space = 744.0;
List<DataTypeBusiness> sorted = sortDataTypesBusiness(dataTypes).toList();
Map<int, pw.TableColumnWidth> map = {};
if (sorted.contains(DataTypeBusiness.logo)) {
space -= 32;
map.addAll(
{sorted.indexOf(DataTypeBusiness.logo): const pw.FixedColumnWidth(32)});
}
if (sorted.contains(DataTypeBusiness.type)) {
space -= 68;
map.addAll(
{sorted.indexOf(DataTypeBusiness.type): const pw.FixedColumnWidth(68)});
}
if (dataTypes.contains(DataTypeBusiness.contactName)) {
space -= 72;
map.addAll({
sorted.indexOf(DataTypeBusiness.contactName):
const pw.FixedColumnWidth(72)
});
}
if (dataTypes.contains(DataTypeBusiness.contactPhone)) {
space -= 76;
map.addAll({
sorted.indexOf(DataTypeBusiness.contactPhone):
const pw.FixedColumnWidth(76)
});
}
double leftNum = 0;
if (dataTypes.contains(DataTypeBusiness.name)) {
leftNum += 1;
}
if (dataTypes.contains(DataTypeBusiness.website)) {
leftNum += 1;
}
if (dataTypes.contains(DataTypeBusiness.contactEmail)) {
leftNum += 1;
}
if (dataTypes.contains(DataTypeBusiness.notes)) {
leftNum += 1;
}
if (dataTypes.contains(DataTypeBusiness.description)) {
leftNum += 3;
}
leftNum = space / leftNum;
if (dataTypes.contains(DataTypeBusiness.name)) {
map.addAll(
{sorted.indexOf(DataTypeBusiness.name): pw.FixedColumnWidth(leftNum)});
}
if (dataTypes.contains(DataTypeBusiness.website)) {
map.addAll({
sorted.indexOf(DataTypeBusiness.website): pw.FixedColumnWidth(leftNum)
});
}
if (dataTypes.contains(DataTypeBusiness.contactEmail)) {
map.addAll({
sorted.indexOf(DataTypeBusiness.contactEmail):
pw.FixedColumnWidth(leftNum)
});
}
if (dataTypes.contains(DataTypeBusiness.notes)) {
map.addAll(
{sorted.indexOf(DataTypeBusiness.notes): pw.FixedColumnWidth(leftNum)});
}
if (dataTypes.contains(DataTypeBusiness.description)) {
map.addAll({
sorted.indexOf(DataTypeBusiness.description):
pw.FixedColumnWidth(leftNum * 3)
});
}
return map;
}
Map<int, pw.TableColumnWidth> _jobColumnSizes(Set<DataTypeJob> dataTypes) {
Map<int, pw.TableColumnWidth> map = {};
List<DataTypeJob> sortedDataTypes = sortDataTypesJob(dataTypes).toList();
if (dataTypes.contains(DataTypeJob.businessName)) {
map.addAll({
sortedDataTypes.indexOf(sortedDataTypes
.where((element) => element == DataTypeJob.businessName)
.first): const pw.FractionColumnWidth(0.2)
});
}
if (dataTypes.contains(DataTypeJob.type)) {
map.addAll({
sortedDataTypes.indexOf(sortedDataTypes
.where((element) => element == DataTypeJob.type)
.first): const pw.FractionColumnWidth(0.1)
});
}
if (dataTypes.contains(DataTypeJob.name)) {
map.addAll({
sortedDataTypes.indexOf(sortedDataTypes
.where((element) => element == DataTypeJob.name)
.first): const pw.FractionColumnWidth(0.2)
});
}
if (dataTypes.contains(DataTypeJob.description)) {
map.addAll({
sortedDataTypes.indexOf(sortedDataTypes
.where((element) => element == DataTypeJob.description)
.first): const pw.FractionColumnWidth(0.3)
});
}
if (dataTypes.contains(DataTypeJob.offerType)) {
map.addAll({
sortedDataTypes.indexOf(sortedDataTypes
.where((element) => element == DataTypeJob.offerType)
.first): const pw.FractionColumnWidth(0.1)
});
}
if (dataTypes.contains(DataTypeJob.wage)) {
map.addAll({
sortedDataTypes.indexOf(sortedDataTypes
.where((element) => element == DataTypeJob.wage)
.first): const pw.FractionColumnWidth(0.15)
});
}
if (dataTypes.contains(DataTypeJob.link)) {
map.addAll({
sortedDataTypes.indexOf(sortedDataTypes
.where((element) => element == DataTypeJob.link)
.first): const pw.FractionColumnWidth(0.2)
});
}
return map;
}
dynamic businessValueFromDataType(
Business business, DataTypeBusiness dataType) {
switch (dataType) {
case DataTypeBusiness.name:
return business.name;
case DataTypeBusiness.description:
return business.description;
case DataTypeBusiness.type:
return getNameFromBusinessType(business.type!);
case DataTypeBusiness.website:
return business.website;
case DataTypeBusiness.contactName:
return business.contactName;
case DataTypeBusiness.contactEmail:
return business.contactEmail;
case DataTypeBusiness.contactPhone:
return business.contactPhone;
case DataTypeBusiness.notes:
return business.notes;
case DataTypeBusiness.logo:
return null;
}
}
dynamic jobValueFromDataType(JobListing job, DataTypeJob dataType) {
switch (dataType) {
case DataTypeJob.name:
return job.name;
case DataTypeJob.description:
return job.description;
case DataTypeJob.type:
return job.type;
case DataTypeJob.offerType:
return job.offerType;
case DataTypeJob.wage:
return job.wage;
case DataTypeJob.link:
return job.link;
case DataTypeJob.businessName:
return null;
}
}
+6
View File
@@ -0,0 +1,6 @@
import 'package:flutter/material.dart';
late String jwt;
const int widescreenWidth = 600;
bool loggedIn = false;
ThemeMode themeMode = ThemeMode.system;
+311
View File
@@ -0,0 +1,311 @@
import 'package:flutter/material.dart';
import 'package:pdf/widgets.dart' as pw;
enum DataTypeBusiness {
logo,
name,
description,
type,
website,
contactName,
contactEmail,
contactPhone,
notes,
}
enum DataTypeJob {
businessName,
name,
description,
type,
offerType,
wage,
link,
}
enum BusinessType {
food,
shop,
outdoors,
manufacturing,
entertainment,
other,
}
enum JobType {
retail,
customerService,
foodService,
education,
maintenance,
manufacturing,
other,
}
enum OfferType { job, internship, apprenticeship }
class JobListing {
int? id;
int? businessId;
String name;
String description;
JobType? type;
OfferType? offerType;
String? wage;
String? link;
JobListing({
this.id,
this.businessId,
required this.name,
required this.description,
this.type,
this.offerType,
this.wage,
this.link,
});
factory JobListing.copy(JobListing input) {
return JobListing(
id: input.id,
businessId: input.businessId,
name: input.name,
description: input.description,
type: input.type,
offerType: input.offerType,
wage: input.wage,
link: input.link,
);
}
}
class Business {
int id;
String? name;
String? description;
BusinessType? type;
String? website;
String? contactName;
String? contactEmail;
String? contactPhone;
String? notes;
String locationName;
String? locationAddress;
List<JobListing>? listings;
Business(
{required this.id,
required this.name,
required this.description,
this.website,
this.type,
this.contactName,
this.contactEmail,
this.contactPhone,
this.notes,
required this.locationName,
this.locationAddress,
this.listings});
factory Business.fromJson(Map<String, dynamic> json) {
List<JobListing>? listings;
if (json['listings'] != null) {
listings = [];
for (int i = 0; i < json['listings'].length; i++) {
listings.add(JobListing(
id: json['listings'][i]['id'],
businessId: json['listings'][i]['businessId'],
name: json['listings'][i]['name'],
description: json['listings'][i]['description'],
type: JobType.values.byName(json['listings'][i]['type']),
wage: json['listings'][i]['wage'],
link: json['listings'][i]['link'],
offerType:
OfferType.values.byName(json['listings'][i]['offerType'])));
}
}
return Business(
id: json['id'],
name: json['name'],
description: json['description'],
type: json['type'] != null
? BusinessType.values.byName(json['type'])
: null,
website: json['website'],
contactName: json['contactName'],
contactEmail: json['contactEmail'],
contactPhone: json['contactPhone'],
notes: json['notes'],
locationName: json['locationName'],
locationAddress: json['locationAddress'],
listings: listings);
}
factory Business.copy(Business input) {
return Business(
id: input.id,
name: input.name,
description: input.description,
website: input.website,
contactName: input.contactName,
contactEmail: input.contactEmail,
contactPhone: input.contactPhone,
notes: input.notes,
locationName: input.locationName,
locationAddress: input.locationAddress,
listings: input.listings);
}
}
IconData getIconFromBusinessType(BusinessType type) {
switch (type) {
case BusinessType.food:
return Icons.restaurant;
case BusinessType.shop:
return Icons.store;
case BusinessType.outdoors:
return Icons.forest;
case BusinessType.manufacturing:
return Icons.factory;
case BusinessType.entertainment:
return Icons.live_tv;
case BusinessType.other:
return Icons.business;
}
}
IconData getIconFromJobType(JobType type) {
switch (type) {
case JobType.retail:
return Icons.shopping_bag;
case JobType.customerService:
return Icons.support_agent;
case JobType.foodService:
return Icons.restaurant;
case JobType.education:
return Icons.school;
case JobType.maintenance:
return Icons.handyman;
case JobType.manufacturing:
return Icons.factory;
case JobType.other:
return Icons.work;
}
}
pw.IconData getPwIconFromBusinessType(BusinessType type) {
switch (type) {
case BusinessType.food:
return const pw.IconData(0xe56c);
case BusinessType.shop:
return const pw.IconData(0xea12);
case BusinessType.outdoors:
return const pw.IconData(0xea99);
case BusinessType.manufacturing:
return const pw.IconData(0xebbc);
case BusinessType.entertainment:
return const pw.IconData(0xe639);
case BusinessType.other:
return const pw.IconData(0xe0af);
}
}
pw.IconData getPwIconFromJobType(JobType type) {
switch (type) {
case JobType.retail:
return const pw.IconData(0xf1cc);
case JobType.customerService:
return const pw.IconData(0xf0e2);
case JobType.foodService:
return const pw.IconData(0xe56c);
case JobType.education:
return const pw.IconData(0xe80c);
case JobType.maintenance:
return const pw.IconData(0xf10b);
case JobType.manufacturing:
return const pw.IconData(0xebbc);
case JobType.other:
return const pw.IconData(0xe8f9);
}
}
String getNameFromBusinessType(BusinessType type) {
switch (type) {
case BusinessType.food:
return 'Food Related';
case BusinessType.shop:
return 'Shops';
case BusinessType.outdoors:
return 'Outdoors';
case BusinessType.manufacturing:
return 'Manufacturing';
case BusinessType.entertainment:
return 'Entertainment';
case BusinessType.other:
return 'Other';
}
}
String getNameFromJobType(JobType type) {
switch (type) {
case JobType.retail:
return 'Retail';
case JobType.customerService:
return 'Customer Service';
case JobType.foodService:
return 'Food Service';
case JobType.education:
return 'Education';
case JobType.maintenance:
return 'Maintenance';
case JobType.manufacturing:
return 'Manufacturing';
case JobType.other:
return 'Other';
}
}
String getNameFromOfferType(OfferType type) {
switch (type) {
case OfferType.job:
return 'Job';
case OfferType.internship:
return 'Internship';
case OfferType.apprenticeship:
return 'Apprenticeship';
}
}
String getLetterFromOfferType(OfferType type) {
switch (type) {
case OfferType.job:
return 'J';
case OfferType.internship:
return 'I';
case OfferType.apprenticeship:
return 'A';
}
}
Color getColorFromOfferType(OfferType type) {
switch (type) {
case OfferType.job:
return Colors.blue;
case OfferType.internship:
return Colors.green.shade800;
case OfferType.apprenticeship:
return Colors.red;
}
}
IconData getIconFromThemeMode(ThemeMode theme) {
switch (theme) {
case ThemeMode.dark:
return Icons.dark_mode;
case ThemeMode.light:
return Icons.light_mode;
case ThemeMode.system:
return Icons.brightness_4;
}
}
+409
View File
@@ -0,0 +1,409 @@
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:fbla_ui/pages/signin_page.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
class BusinessSearchBar extends StatefulWidget {
final String searchTextHint;
final Widget filterIconButton;
final void Function(String) setSearchCallback;
const BusinessSearchBar(
{super.key,
required this.setSearchCallback,
required this.searchTextHint,
required this.filterIconButton});
@override
State<BusinessSearchBar> createState() => _BusinessSearchBarState();
}
class _BusinessSearchBarState extends State<BusinessSearchBar> {
TextEditingController controller = TextEditingController();
@override
Widget build(BuildContext context) {
return SizedBox(
width: 450,
height: 50,
child: SearchBar(
hintText: widget.searchTextHint,
controller: controller,
backgroundColor: WidgetStateProperty.resolveWith((notNeeded) {
return Theme.of(context).colorScheme.surfaceContainer;
}),
onChanged: (query) {
widget.setSearchCallback(query);
},
leading: const Padding(
padding: EdgeInsets.only(left: 8.0),
child: Icon(Icons.search),
),
trailing: [
if (controller.text != '')
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
controller.text = '';
widget.setSearchCallback('');
},
),
widget.filterIconButton
]),
);
}
}
class FilterChips extends StatefulWidget {
final Set<JobType>? selectedJobChips;
final Set<BusinessType>? selectedBusinessChips;
const FilterChips(
{super.key, this.selectedJobChips, this.selectedBusinessChips});
@override
State<FilterChips> createState() => _FilterChipsState();
}
class _FilterChipsState extends State<FilterChips> {
List<Padding> filterChips() {
List<Padding> chips = [];
if (widget.selectedJobChips != null) {
for (var type in JobType.values) {
chips.add(Padding(
padding: const EdgeInsets.only(left: 4.0, right: 4.0),
child: FilterChip(
showCheckmark: false,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
label: Text(getNameFromJobType(type)),
selected: widget.selectedJobChips!.contains(type),
onSelected: (bool selected) {
setState(() {
if (selected) {
widget.selectedJobChips!.add(type);
} else {
widget.selectedJobChips!.remove(type);
}
});
}),
));
}
} else if (widget.selectedBusinessChips != null) {
for (var type in BusinessType.values) {
chips.add(Padding(
padding: const EdgeInsets.only(left: 4.0, right: 4.0),
child: FilterChip(
showCheckmark: false,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
label: Text(getNameFromBusinessType(type)),
selected: widget.selectedBusinessChips!.contains(type),
onSelected: (bool selected) {
setState(() {
if (selected) {
widget.selectedBusinessChips!.add(type);
} else {
widget.selectedBusinessChips!.remove(type);
}
});
}),
));
}
}
return chips;
}
@override
Widget build(BuildContext context) {
return Wrap(
children: filterChips(),
);
}
}
class MainSliverAppBar extends StatefulWidget {
final bool widescreen;
final Widget filterIconButton;
final void Function(String) setSearch;
final void Function() themeCallback;
final void Function() generatePDF;
final void Function(bool) updateLoggedIn;
final String searchHintText;
const MainSliverAppBar({
super.key,
required this.widescreen,
required this.setSearch,
required this.searchHintText,
required this.themeCallback,
required this.filterIconButton,
required this.updateLoggedIn,
required this.generatePDF,
});
@override
State<MainSliverAppBar> createState() => _MainSliverAppBarState();
}
class _MainSliverAppBarState extends State<MainSliverAppBar> {
@override
Widget build(BuildContext context) {
return SliverAppBar(
title: widget.widescreen
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: BusinessSearchBar(
setSearchCallback: widget.setSearch,
searchTextHint: widget.searchHintText,
filterIconButton: widget.filterIconButton,
),
)
// const PreferredSize(
// preferredSize: Size(144, 0), child: SizedBox())
],
)
: const Text('Job Link'),
toolbarHeight: 70,
stretch: false,
backgroundColor: Theme.of(context).colorScheme.surface,
pinned: true,
// floating: true,
scrolledUnderElevation: 0,
centerTitle: !widget.widescreen,
expandedHeight: widget.widescreen ? 70 : 120,
bottom: _getBottom(widget.widescreen),
leading: !widget.widescreen
? IconButton(
icon: Icon(getIconFromThemeMode(themeMode)),
onPressed: () {
setState(() {
widget.themeCallback();
});
},
)
: null,
actions: [
IconButton(
icon: const Icon(Icons.file_download_outlined),
onPressed: widget.generatePDF,
),
IconButton(
icon: const Icon(Icons.help),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('About'),
backgroundColor: Theme.of(context).colorScheme.surface,
content: SizedBox(
width: 500,
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Welcome to my FBLA 2024 Coding and Programming submission!\n\n'
'MarinoDev Job Link aims to provide comprehensive details of businesses and community partners'
' for Waukesha West High School\'s Career and Technical Education Department.\n\n'),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Git Repo:'),
Text(
'https://git.marinodev.com/MarinoDev/FBLA24\n',
style: TextStyle(color: Colors.blue)),
],
),
onTap: () {
launchUrl(Uri.https('git.marinodev.com',
'/MarinoDev/FBLA24'));
},
),
),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Please direct any questions to'),
Text('drake@marinodev.com',
style: TextStyle(color: Colors.blue)),
],
),
onTap: () {
launchUrl(
Uri.parse('mailto:drake@marinodev.com'));
},
),
)
],
),
),
),
actions: [
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
}),
],
);
});
},
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
icon: loggedIn
? const Icon(Icons.account_circle)
: const Icon(Icons.login),
onPressed: () {
if (loggedIn) {
var payload = JWT.decode(jwt).payload;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Theme.of(context).colorScheme.surface,
title: Text('Hi, ${payload['username']}!'),
content: Text(
'You are logged in as an admin with username ${payload['username']}.'),
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Logout'),
onPressed: () async {
final prefs =
await SharedPreferences.getInstance();
prefs.setBool('rememberMe', false);
prefs.setString('username', '');
prefs.setString('password', '');
widget.updateLoggedIn(false);
Navigator.of(context).pop();
}),
],
);
});
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
SignInPage(refreshAccount: widget.updateLoggedIn)));
}
},
),
),
],
);
}
PreferredSizeWidget? _getBottom(bool widescreen) {
if (!widescreen) {
return PreferredSize(
preferredSize: const Size.fromHeight(0),
child: SizedBox(
height: 70,
child: Padding(
padding: const EdgeInsets.all(10),
child: BusinessSearchBar(
filterIconButton: widget.filterIconButton,
setSearchCallback: widget.setSearch,
searchTextHint: widget.searchHintText,
),
),
),
);
}
return null;
}
}
class ContactInformationCard extends StatelessWidget {
final Business business;
ContactInformationCard({super.key, required this.business});
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0),
child: Text(
business.contactName!,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
],
),
if (business.contactPhone != null)
ListTile(
leading: const Icon(Icons.phone),
title: Text(business.contactPhone!),
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Theme.of(context).colorScheme.surface,
title: Text('Contact ${business.contactName}'),
content: Text(
'Would you like to call or text ${business.contactName}?'),
actions: [
TextButton(
child: const Text('Text'),
onPressed: () {
launchUrl(
Uri.parse('sms:${business.contactPhone}'));
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Call'),
onPressed: () async {
launchUrl(
Uri.parse('tel:${business.contactPhone}'));
Navigator.of(context).pop();
}),
],
);
});
},
),
if (business.contactEmail != null)
ListTile(
leading: const Icon(Icons.email),
title: Text(business.contactEmail!),
onTap: () {
launchUrl(Uri.parse('mailto:${business.contactEmail}'));
},
),
],
),
);
}
}
+10 -10
View File
@@ -308,10 +308,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.5" version: "2.2.6"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
@@ -348,10 +348,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: pdf name: pdf
sha256: "243f05342fc0bdf140eba5b069398985cdbdd3dbb1d776cf43d5ea29cc570ba6" sha256: "81d5522bddc1ef5c28e8f0ee40b71708761753c163e0c93a40df56fd515ea0f0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.10.8" version: "3.11.0"
pdf_widget_wrapper: pdf_widget_wrapper:
dependency: transitive dependency: transitive
description: description:
@@ -372,10 +372,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.4" version: "3.1.5"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -396,10 +396,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: printing name: printing
sha256: "1c99cab90ebcc1fff65831d264627d5b529359d563e53f33ab9b8117f2d280bc" sha256: cc4b256a5a89d5345488e3318897b595867f5181b8c5ed6fc63bfa5f2044aec3
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.12.0" version: "5.13.1"
qr: qr:
dependency: transitive dependency: transitive
description: description:
@@ -412,10 +412,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: rive name: rive
sha256: "03bae056e67a5c98f9523c34f8ea63c15b323426a7e8d76d7ebb93066d4df6dc" sha256: "0342c9cd3c83ceeee4ad9246b98d628a2e9abd9d615acf69fa81fbbcf84a36ae"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.5" version: "0.13.8"
rive_common: rive_common:
dependency: transitive dependency: transitive
description: description:
+2
View File
@@ -73,6 +73,8 @@ flutter:
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
assets: assets:
- assets/mdev_triangle_loading.riv - assets/mdev_triangle_loading.riv
- assets/MarinoDev.svg
- assets/Triangle256.png
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg